Solid State KV

SolidState KV is a graph-based realtime replication engine for creating offline first multiplayer applications. SolidState can replicate data peer-to-peer, through a persitent triplestore, or on Solid PODs.

Explore how it works on the KV Playground

Table of Contents

  1. DB
  2. Entities
  3. Persist Data
    1. Post
    2. Put
    3. Patch
    4. Delete
  4. Relationships
  5. Typed Values
  6. Schemas
  7. Getting Entities
    1. Get Data Shapes
    2. Get Relationships
    3. Get Reverse Relationships

DB

SolidState is an offline-first, realtime, multiplayer sync engine for RDF triples.

Create a local store:

import SolidState from "solidstate"
const db = new SolidState()

Assume something like, SPARQL endpoint or Local storage; we can persist multiple graphs seperate from each other as named graphs. Do we want to do that by default? Do we want to segregate them by db id or app id?

Replication & Sync

We want to replicate our local store to other devices, as well as servers. Some places where we want to sync things:

  1. Other browser sessions. How do our browser sessions find each other?
  2. Centralized triplestores, like Jena, Triply, AWS Neptune, or Oxigraph. How do we sync that?
  3. Solid PODs attached to WebID, like https://solidid.stucco.software/ How do we sync that?

Something like zero is a microservice that sits in front of a store. gun.eco is honestly similar, with peers connecting to a service that handles signalling and persistence. A solidstate microservice would be able to handle the peer-to-peer connections as well as emit events when things change, then provide configuration options to attach to triplestores/pods, as well as manage access control.

Ideally we want ALL OF THE ABOVE AT THE SAME TIME, or at least 1 AND (2 OR 3).

// pouch style
const db = new SolidState()
const remote = new SolidState('//triplestore.example.com')
db.sync(remote)

// gun.eco style
const db = SolidState(['//triplestore.example.com'])
// or
const db = SolidState({peers: ['//triplestore.example.com']})

Conflict Resolution

Create a metadata subgraph to handle either pouch-style commit and revision history or gun.eco style “hypothetical amnesia machine” vector conflicts. It would be cool to have:

  1. Full subject revision history
  2. Flat access to most current subject state

Entities

Entities are stored as flat objects, that is sets of key:value pairs. Values can be of any given type. Nested objects are created by managing relationships between entities.

Entities have two special keys: @id and @type.

The @id key is used to uniquely identify the entity.

The @type key is used to classify or attach a schema to the entity.

Persist Data

An Entity is identified by its @id. Create a new item with db.post.

let ref = await db.post("hummus", {
  "@type": "Food",
  "ingredient": "Chickpeas and Lemon"
})
ref = {
  "@id": "hummus",
  "@type": "Food",
  "ingredient": "Chickpeas and Lemon"
}

Post

Use db.post to replace an entire entity with a new value:

let ref = await db.post("hummus", {
  "@type": "Dish",
  "ingredient": "Garbanzo Beans"
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "Garbanzo Beans"
}

Put

Replace the value of a given key with db.put.

let ref = await db.put('hummus, {
  "ingredient": "Chickpeas",
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "Chickpeas"
}
Put and Post are similar but distinct! Post will replace the entire Entity, but Put will replace jut the specified keys.

Patch

Add a value to a key with db.patch

let ref = await db.patch('hummus', {
  ingredient: "Lemon",
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": ["Chickpeas", "Lemon"]
}

Delete

Delete a key/value pair with db.delete

let ref = await db.delete(@id, {
  "ingredient": "Lemon",
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "Chickpeas"
}

Or delete the entire entity

let ref = await db.delete('hummus')
ref = null

Relationships

Entities can be related to each other.

let hummus = await db.post("hummus", {
  "@type": "Dish"
})
let chickpeas = await db.post("chickpeas", {
  "@type": "Ingredient",
  "aka": "Garbanzo Beans"
})

let ref = await db.put('hummus, {
  "ingredient": {
    "@id": "chickpeas"
  }
})
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "ingredient": "chickpeas"
}

Typed Values

Values can be strings, booleans, and numbers. Types can be specified as well:

db.put("chickpeas", {
  "aka": {
    "@type": string,
    "@value": "Garbs"
  }
})
db.put("chickpeas", {
  "gramsPerUnit": {
    "@type": number,
    "@value": 42
  }
})
db.put("chickpeas", {
  "vegetarian": {
    "@type": boolean,
    "@value": true
  }
})

Values can also be typed as date, time, datTime, uri, integer, float, decimal, or double.

Schemas

Entities can be given schemas with the @type key. A @type can be defined with a schema:

let dishSchema = await db.schema("Dish", {
  "name": String,
  "ingredient": ID
})

let ingredientSchema = await db.schema("Ingredient", {
  "name": String,
  "aka": String,
  "vegetarian": Boolean
})

Keys can also be given data:

let ref = await db.post("vegetarian", {
  "description": "Does this ingredient made entirely from non-animal products?"
})

Okay but _why_ would you do this?

Keys can also be given schemas:

let vegSchema = await db.schema('vegetarian', {
  "description": String,
  "@domain": "Ingredient",
  "@range": Boolean
})

See above. Does this enable static type checking by surfacing errors? Can we use this to do introspection?

Getting Entities

Get a single item by @id with db.get

let ref = await db.get('hummus')
ref = {
  "@id": "hummus",
  "@type": "Dish",
  "name": "Hummus",
  "ingredient": ["chickpeas", "lemon"]
}

Get Data Shapes

Get a single item with specified keys:

let ref = await db.get('hummus', {
  "ingredient": ""
})
ref = {
  "@id": "hummus",
  "ingredient": ["chickpeas", "lemon"]
}

Get Relationships

Get related items with specified keys:

let ref = await db.get('hummus', {
  "ingredient": {
    "name": "",
    "aka": ""
  }
})
ref = {
  "@id": "hummus",
  "ingredient": [{
    "name": "Chickpeas",
    "aka": ["Garbanzos", "Garbs"]
  },{
    "name": "Lemon"
  }]
}

Get Reverse Relationships

Relationships can be reversed:

let ref = await db.get('chickpeas', {
  "name": "",
  "usedIn": {
    "@reverse": "ingredient"
  }
})
ref = {
  "@id": chickpeas,
  "usedIn": "hummus"
}