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:

ts
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:

ts
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:

ts
RemoteServer.write(EditProject, writer(Db.Project)) // `writer` from foldkit-remote-drizzle

The 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.