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

Could this approach also be used to make a better websocket integration?

I don’t think so. The point of websocket is to have server push messages and have a subscription on the elm side. If you don’t need that asymmetric style of communication, you can just use http (with concurrent task or without).

Even trying to shoehorn concurrent-task for the parts of websocket that look like a request/response is not the best I think. For example if you want to open with a task, it might succeed now, but you get an error from a third port subscribing to events later.

An appropriate approach could reuse the same kind of setup though. A pair of ports, a dual package Elm/JS with an easy install and a standard way of handling it. Maybe something like this:

port websocketOut : Encode.Value -> Cmd msg
port websocketIn : (Decode.Value -> msg) -> Sub msg

-- Commands (sent via websocketOut)
type OutMsg
    = Open { id : String, url : String }
    | Send { id : String, data : String }
    | Close { id : String, code : Int, reason : String }

-- Events (received via websocketIn)
type InMsg
    = Opened { id : String }
    | Message { id : String, data : String }
    | Closed { id : String, code : Int, reason : String }
    | Error { id : String, message : String }
    | Reconnecting { id : String, attempt : Int, delayMs : Int }
    | Reconnected { id : String }

Then the JS side of the package could provide a function like:

function createWebSocketManager(ports) {
  const sockets = new Map(); // Initialize a socket manager
  ports.websocketOut.subscribe((cmd) => {
    // Handle commands of type open/send/close
    // Listen to socket events and forward them
  });
}

This way can just import the JS package and call that function with a { websocketIn, websocketOut } object containing the two ports of your app for the websocket.

This is roughly what billstclair/elm-websocket-client or kageurufu/elm-websockets are doing already. I could give a try at an alternative package if I get some need for it. Will see.

1 Like

I’ve added these three other features, because I suspect I’ll have a need for them very soon.

  1. Key ranges
  2. Secondary indexes
  3. Posix (time) keys

These features let you filter data at the IndexedDB level instead of fetching everything into Elm.

Updated package API docs: Elm Doc Preview

Key ranges

Query subsets of a store by primary key without loading all records.

Idb.getAllInRange db store (Idb.between (Idb.IntKey 1) (Idb.IntKey 100)) decoder
Idb.getAllKeysInRange db store (Idb.from (Idb.IntKey 50))
Idb.countInRange db store (Idb.upTo (Idb.IntKey 99))
Idb.deleteInRange db store (Idb.below (Idb.IntKey 10))

Six constructors cover all IDBKeyRange variants: only, from (>=), above (>), upTo (<=), below (<), between (>= and <=).

Secondary indexes

Define indexes on value fields for lookups beyond the primary key.

byTimestamp : Idb.Index
byTimestamp = Idb.defineIndex "by_timestamp" "timestamp"

eventsStore : Idb.Store Idb.GeneratedKey
eventsStore =
    Idb.defineStore "events"
        |> Idb.withAutoIncrement
        |> Idb.withIndex byTimestamp

Query through an index with a key range. Here we use IntKey instead of PosixKey because we most likely encode the timestamp as an Int inside the Value with something like Encode.int (Time.posixToMillis time).

Idb.getByIndex db eventsStore byTimestamp (Idb.between (Idb.IntKey startMs) (Idb.IntKey endMs)) decoder
Idb.getKeysByIndex db eventsStore byTimestamp range
Idb.countByIndex db eventsStore byTimestamp range

Index queries return primary keys, so results work directly with get, delete, etc.

Indexes are managed automatically during schema upgrades: new indexes are created, removed indexes are deleted. Use uniqueIndex to enforce uniqueness and multiEntryIndex for array-valued fields.

PosixKey

A new Key variant for timestamps, stored as native Date objects in IndexedDB.

Idb.PosixKey (Time.millisToPosix 1700000000000)

Round-trips losslessly: Elm PosixKey → JS Date → IndexedDB → JS instanceof Date → Elm PosixKey. Use keyToPosix to extract the Time.Posix value.

This is for primary keys. For timestamp fields inside values (used via secondary indexes), store them as integers and query with IntKey.

Migration

Bump your schema version. Store and index changes are applied automatically by open. No code changes needed for existing operations – all new functions are additive.

The updated example TODO app

Now showcases these new features, by filtering an event store.
Link to the app: Elm IndexedDB Example

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.