New IndexedDB package (based on elm-concurrent-task)

Hi, I’m about to publish a package to make it easier to use IndexedDB from elm apps. Would love some feedback before hitting the publish button of version 1.0.0.

6 Likes

Love that you’re doing this! I don’t have a concrete use case right now, but definitely something I’ve wanted in the past. elm-concurrent-task wasn’t around when I last looked at this and it definitely seems to be a key piece of the puzzle. Again, thank you!

3 Likes

With all the new LLM powers we have, I’ve been doing a lot of vibecoding for small utility mobile apps, local-first. And it pains me that I’ve been using SolidJS instead of Elm for it (even though solid is quite nice). So I’m trying to fill the gaps to be able to unleash claude with elm powers ^^. This means better support of a few things I need. And for local-first PWAs, IndexedDB is a must have!

So this IndexedDB package is a first step in my larger goal :slight_smile:

2 Likes

Had a leaf through the Api and example and it looks great! I think you’re the first to publish a lib based on concurrent-task :slight_smile:

2 Likes

Don’t want to diverge too much from the topic, happy to discuss in a separate thread/space as well. I’m curious if you’ve hit any issues with your PWA work and iOS or Android? Either IndexedDB related or anything else.

Mostly, I’ve had trouble with notifications. But yeah let’s move that to the slack.

Thank you for building this!!!

I have a couple of suggestions to improve type safety and ergonomics but I am pretty sure you intentionally decided against what I’ll suggest. Some (maybe all) of my suggestions may be based on ignorance of IndexedDb or the intended use cases.

The Store type has a phantom type argument to protect portions of the Api related to the three different key categories of Explicit, Inline, and Generated which is really great. However

  1. When using get one can pass an Int, Float or compound key to a store that has a String key and that seems like an outright run-time error.
  2. When using get or putAt one is limited to simple data types like string, int, etc. so one cannot use a semantically rich id type like type PersonId = PersonId String or type PetId = PetId String.
  3. Users have to include the decoder for every get and provide a Value for write operations which feels type-unsafe and a little tedious. Though perhaps this is a feature and not a bug in that it allows for flexibility? If so then discussing the use cases in the documentation might be nice.

What if we enhanced the type signature of store a wee bit?

type alias KeyCodec key =
    { encode : key -> Key -- this makes it so that you can only produce appropriate keys
    , decode : Key -> Result String key
    }

-- keyCategory would be your marker types ExplicitKey, InineKey, GeneratedKey
type Store keyCategory key model
    = Store
        { config : StoreConfig -- holds the name, key path and auto increment 
        , keyCodec : KeyCodec key
        , modelCodec : 
             { encoder : model -> Value
             , decoder : Decoder model
             }
        }

Now there would be enough type information in the store to rigorously type the get and putAt operations with respect to the key and users wouldn’t have to pass the encoder/decoder every time.

Of course, this makes a couple of assumptions

  1. This is to be a high level library that is used by the application code directly. If this is intended as a lower level library put behind an opaque data type (PetStoreDb or TodoDb) then you might want the flexibility and both the type safety and ergonomic would be moot because 96% of the code would go through the explicitly defined / strict application or domain Api.
  2. I am assuming that all keys in a store should have a homogeneous type.