Remote / foldkit-remote-server
Mutations
A Mutation Source changes server-owned state. It returns the protocol output plus any normalized entity/connection changes that should settle the client's cache immediately:
import { Effect } from 'effect'
import { Mutation } from 'foldkit-remote'
const RenameProject = Mutation.make('RenameProject', {
Input: Schema.Struct({ id: Schema.String, name: Schema.String }),
Output: Schema.Struct({ id: Schema.String }),
})
const RenameProjectSource = RemoteServer.mutation(
RenameProject,
({ input, principal }) =>
renameProject(input, principal).pipe(
Effect.map(output => ({
output,
entities: [
Entity.patch(Project.ref(input.id), { name: input.name }),
],
})),
),
)The input is decoded before the Source runs and the output is encoded before it
crosses the wire. A Source refuses with a value of the mutation's Refusal:
Effect.fail(RemoteServer.refuse(SetPrice, { _tag: 'Field', key: 'price', reason: 'too low' })).
The value is type-checked against that mutation, encoded by its schema, and
sent beside a message (a 422 over JSON); a refusal made for another mutation,
or not one the mutation declares, is sent as an invalid refusal instead.
A refusal of who is asking, rather than of what they asked, is
Effect.fail(RemoteServer.forbid('Only an admin may archive.')): sent as
Forbidden with the reason (a 403 over JSON), or with none when forbid() is
given none, so the refusal says nothing of why. It is the same for every
mutation, so it needs no Refusal declared. The Source is also given requestId, the client's id for the
request, which is the same on every retry of it: record a write under it, and a
retry can find that the write was made. Nothing deduplicates by it yet. A Source
may additionally return connection changes:
RemoteServer.prepend(connection, Project.ref(id))
RemoteServer.append(connection, Project.ref(id))
RemoteServer.remove(connection, Project.ref(id))Those confirmed changes replace the corresponding optimistic connection layers on the client.
A mutation declared as a Write (Mutation.write) needs no handler.
RemoteServer.write(mutation, writer) binds the input to the write, only the
keys the client named when it named some, and hands the row's id and new values
to an EntityWriter, which writes them and answers the row as written:
RemoteServer.write(EditProject, writer(Db.Project)) // `writer` from foldkit-remote-drizzleThe answer is the mutation's patch, or for a delete its deletion. With
expect, the writer is told the revision the row was read at; no row at it is
refused as Refusal.conflict. An update of no row fails; an insert of an id
already holding what it writes answers that row, and a delete of a row already
gone succeeds, so a retry of either is safe. An insert of an id holding
anything else fails: that row is not its answer. The writer must be for the write's own Entity,
which is checked where the mutation is served.
A writer with a read (the row as a principal may see it, under the read
rule its Source applies) answers through it: the patch is the written row as
its writer now sees it, and a row the write moved out of their sight answers
in deleted, so the client takes it out of every list rather than placing
it where the server would not list it. A writer may refuse with
RemoteServer.forbid, as foldkit-guard's GuardServer.writer does. Its
linked answers a row's links as stored, whatever a reader may see of the
rows they point at, so such a guard can tell a write that unlinks a hidden row
from one that changes nothing; foldkit-remote-drizzle's writer has both.
Remote mutations are immediate requests against server-owned state. They are
not a durable offline intent log; client-authored edits that must survive offline
belong to foldkit-sync.