Remote / foldkit-remote
Normalized entities and selections
Remote stores an entity once by identity, regardless of how many Surfaces read it. Consumers select different views of the same normalized fact.
Relations are references, not nested copies:
const User = Entity.make(
'User',
Schema.Struct({
id: Schema.String,
name: Schema.String,
}),
)
const Project = Entity.make(
'Project',
Schema.Struct({
id: Schema.String,
name: Schema.String,
owner: Entity.ref(User),
}),
)
const UserSummary = User.select({ id: true, name: true })
const ProjectSummary = Project.select({
id: true,
name: true,
owner: UserSummary,
})The store remains normalized:
Project:p1 name PRESENT owner PRESENT -> User:u7
User:u7 name PRESENTThe Projection assembles the nested consumer value by following the reference.
An unknown field or a nested Selection for the wrong entity is a type error.
Recursive relations remain finite because the entity schema stores references
(Entity.ref(User) / Entity.refTo('Node')), not recursively inlined schemas.
Entities declared with foldkit-entity
foldkit-entity declares a domain without Remote in it:
fields, relations as navigation edges, derived members. Remote.make registers
those Entities, and Data.get, Data.live, and a query's select take their
Selections. Remote compiles both into the descriptor and Selection above, so
the store, the planner, and the server see nothing new.
import { Schema } from 'effect'
import { Entity, Relation } from 'foldkit-entity'
import { Remote } from 'foldkit-remote'
const User = Entity.define('User', Schema.Struct({ id: Schema.String, name: Schema.String }))
const Project = Entity.define('Project', Schema.Struct({ id: Schema.String, name: Schema.String }))
const Work = Entity.relate({ User, Project }, { Project: { owner: Relation.one(User) } })
const ProjectCard = Entity.select(Work.Project, {
name: true,
owner: Entity.select(Work.User, { name: true }),
})
const Data = Remote.make({ model: App.model.remote, entities: [Work.User, Work.Project] })
const card = Data.get(ProjectCard, 'p1') // Projection<Model, RemoteData<{ name; owner: { name } }>>- A relation becomes a ref field:
onea ref, an optionalonea nullable ref,manyan array of refs. A derived member becomes a field the server supplies. - The Entity needs an
idfield; Remote keys the store by it. When that field is branded,Data.getandData.livetake that type for an Entity Selection: another Entity's id, or plain text, does not compile. - Register the Entities of one
Entity.relateresult and select from that same result;Object.values(Work)registers them all. Entity.from(entity)andSelection.from(selection)are the compile steps, for when you need the descriptor itself:patchandrefin a mutation handler.Entity.page(selection, window)in an Entity Selection is Remote'sSelection.connection: the window travels with the read, and the server answers with a page of refs. A page of a whole list is read under a name of its own,comments@first=10, so one view can show every comment while another shows the first ten of the same post: they are two fields to the store, fetched in one batch, merged and refreshed each on its own. A write to the list (a mutation's patch, a live change) marks its pages stale, so they are read again. A cursor is not part of the name: a page read from a cursor continues its page. The server refuses a request that pages one relation more than four ways, and a field's own name may not contain@.
Entity.make with Entity.ref keeps working, and both kinds can share one
domain. Both packages export Entity; a module that needs Entity.from beside
Entity.define aliases one of them.
Presence is tracked separately from the JavaScript value. These are distinct:
field missing
field present with undefined
field present with null
field stale
entity not foundRemote never infers presence from value === undefined.