Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/unreleased/1720.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- `protocol/json_rpc`: `errorResponseOf` is now exported and joined by
`successResponseOf`, so protocols layered on this module build response
envelopes with the owner's constructors instead of their own.
- `protocol/json_rpc/types`: new `SuccessResponse` and `ErrorResponse` — the two
branches of the `Response` union, for a caller that holds one and wants
`.result` or `.error` without an `in` check.
4 changes: 2 additions & 2 deletions fjs/effects/todo/map-step-combinator.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Two shapes, both the same thing:
*Projection* (`x => pure(f(x))`):

```ts
// fjs/protocol/mcp/module.f.mjs:347-350
// fjs/protocol/mcp/module.f.mjs:411-414
: step(
handlers.toolsList(pr),
r => pure(_okResponse(id)(r)),
r => pure(successResponseOf(id)(r)),
)
```

Expand Down
39 changes: 35 additions & 4 deletions fjs/protocol/json_rpc/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @module
*
* @import { Unknown } from '../../media/json/types.ts'
* @import { Id, RpcError, Handlers, Response } from './types.ts'
* @import { Id, RpcError, Handlers, Response, SuccessResponse, ErrorResponse } from './types.ts'
*/

import { at } from '../../types/object/module.f.mjs'
Expand Down Expand Up @@ -78,8 +78,39 @@ export const methodNotFound = rpcError(-32601)('Method not found')
export const invalidParams = rpcError(-32602)('Invalid params')
export const internalError = rpcError(-32603)('Internal error')

/** @type {(id: Id) => (error: RpcError) => Response} */
const errorResponseOf = id => error => ({ jsonrpc, error, id })
/**
* The error half of the response envelope: `{ jsonrpc, error, id }`.
*
* Exported as one of a pair with {@link successResponseOf} — the `Response`
* schema, `jsonrpc`, `Id` and `RpcError` are all owned here, so the two shapes
* built from them are too. Every protocol layered on this module needs both
* (`fjs/protocol/mcp` and its stdio transport are the two consumers today),
* and a private constructor is what made each of them re-roll its own.
*
* It answers the `Response` union rather than {@link ErrorResponse}, the branch
* it always builds. That is deliberate: every consumer in the tree is a
* dispatcher answering either arm — `dispatch` here, `mcpStep` and the stdio
* transport in `fjs/protocol/mcp` — and `Handle` is defined in terms of the
* union, so the branch type would have to be widened again at each of them.
* {@link ErrorResponse} is exported for a caller that does want it.
*
* @type {(id: Id) => (error: RpcError) => Response}
*/
export const errorResponseOf = id => error => ({ jsonrpc, error, id })

/**
* The success half of the response envelope: `{ jsonrpc, result, id }`.
*
* The `…Of` suffix pairs with {@link errorResponseOf}, and both name the
* already-exported `successResponse` / `errorResponse` schemas they build a
* value of.
*
* It answers the union for the reason {@link errorResponseOf} does, and
* {@link SuccessResponse} names the branch for a caller that wants it.
*
* @type {(id: Id) => (result: Unknown) => Response}
Comment thread
sergey-shandar marked this conversation as resolved.
*/
export const successResponseOf = id => result => ({ jsonrpc, result, id })
Comment thread
sergey-shandar marked this conversation as resolved.

/**
* Dispatches an already-parsed JSON-RPC value against `handlers`.
Expand Down Expand Up @@ -110,6 +141,6 @@ export const dispatch = handlers => value => {
}
const [t2, result] = handler(params)
return t2 === 'ok'
? { jsonrpc, result, id }
? successResponseOf(id)(result)
: errorResponseOf(id)(result)
}
29 changes: 29 additions & 0 deletions fjs/protocol/json_rpc/proof.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
methodNotFound,
invalidParams,
internalError,
response,
errorResponseOf,
successResponseOf,
} from './module.f.mjs'

/** @type {(r: readonly [string, unknown]) => boolean} */
Expand Down Expand Up @@ -102,4 +105,30 @@ export const proof = {
assertEq(r, null)
},
},

// The two envelope constructors are public API, so they are proven
// directly rather than only through `dispatch`. Each result is parsed back
// with this module's own `response` schema, which is the fact worth
// pinning: the constructor and the schema describing the shape it builds
// cannot drift apart.
responseOf: {
success: () => {
const r = successResponseOf(1)('pong')
assertEq(r.jsonrpc, '2.0')
assert('result' in r && r.result === 'pong', r)
assertEq(r.id, 1)
assert(isOk(parse(response)(r)), r)
},
// A non-`null` id on purpose: an `id` forced to `null` is a mutation
// this case would not see if it asked for `null` to begin with, and a
// string exercises the other arm of `Id` besides. `errorResponseOf`'s
// `null` id is covered by `dispatch.invalidRequest`.
error: () => {
const r = errorResponseOf('abc')(parseError)
assertEq(r.jsonrpc, '2.0')
assert('error' in r && r.error.code === -32700, r)
assertEq(r.id, 'abc')
assert(isOk(parse(response)(r)), r)
},
},
}
9 changes: 5 additions & 4 deletions fjs/protocol/json_rpc/todo/effectful-dispatch-skeleton.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ const handler = at(method)(handlers)
if (handler === null) { return errorResponseOf(id)(methodNotFound) }
```

Effectful `mcpStep` (`fjs/protocol/mcp/module.f.mjs:264-287`):
Effectful `mcpStep` (`fjs/protocol/mcp/module.f.mjs:314-346`):

```js
const [t, message] = decodeRequest(value)
if (t === 'error') { return pure(_errResponse(null)(invalidRequest)) }
if (t === 'error') { return pure(errorResponseOf(null)(invalidRequest)) }
const { id, method, params } = message
if (id === undefined) {
if (method === 'notifications/initialized') { ... }
Expand Down Expand Up @@ -78,6 +78,7 @@ fold into the 66D envelope work if it touches the same lines anyway.

## Related

- `fjs/protocol/json_rpc/todo/response-constructors.md` — the envelope
*constructors*; this issue is the envelope *routing*. Complementary.
- `errorResponseOf` / `successResponseOf` (`../module.f.mjs`) — the envelope
*constructors*, exported from this module; this issue is the envelope
*routing*. Complementary, and the skeleton below builds on them.
- `fjs/protocol/mcp/todo/README.md` (66D) — per-method validate/response arms.
101 changes: 0 additions & 101 deletions fjs/protocol/json_rpc/todo/response-constructors.md

This file was deleted.

16 changes: 15 additions & 1 deletion fjs/protocol/json_rpc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

import type { Unknown } from '../../media/json/types.ts'
import type { Result } from '../../types/result/types.ts'
import type { _id, request, error as errorSchema, response } from './module.f.mjs'
import type {
_id, request, error as errorSchema, response, successResponse, errorResponse,
} from './module.f.mjs'
import type { Ts } from '../../types/rtti/ts/types.ts'

export type Id = Ts<typeof _id>
Expand All @@ -26,6 +28,18 @@ export type RpcError = Ts<typeof errorSchema>
*/
export type Response = Ts<typeof response>

/**
* The success branch of {@link Response}: `result` is present, `error` is not.
*
* `successResponseOf` answers the `Response` union, not this — every consumer
* in the tree is a dispatcher that returns either arm. Name this where a
* caller genuinely holds one branch and wants `.result` without an `in` check.
*/
export type SuccessResponse = Ts<typeof successResponse>

/** The error branch of {@link Response}, the mirror of {@link SuccessResponse}. */
export type ErrorResponse = Ts<typeof errorResponse>

/** A method implementation: maps `params` to a result or an `RpcError`. */
export type Handler = (params: Unknown | undefined) => Result<Unknown, RpcError>

Expand Down
Loading
Loading