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
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:
- Other browser sessions. How do our browser sessions find each other?
- Centralized triplestores, like Jena, Triply, AWS Neptune, or Oxigraph. How do we sync that?
- 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:
- Full subject revision history
- 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"
}