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
29 changes: 29 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,35 @@ package, and `raco rivet verify` behavior identical on all three platforms.

Projects created before this feature remain valid because all three settings are optional. To embed a Windows icon in an older generated host, add Rivet's conditional `RIVET_WINDOWS_ICON_RC` `ResourceCompile` item from the current host template or regenerate the host project while preserving application UI sources.

## Application data directories

Resources are read-only and ship with the application. Mutable user data —
documents, preferences, caches, databases — is written at runtime by the
product, and Rivet deliberately provides no API for it: the location follows
each platform's convention and stays under the product's control.

- **Windows**: `%APPDATA%\<publisher>\<name>` (roaming; use `%LOCALAPPDATA%`
for large caches).
- **macOS**: `~/Library/Application Support/<identifier>` (plus `~/Library/Caches/<identifier>`
for purgeable caches).
- **Linux**: `$XDG_DATA_HOME/<identifier>` when `XDG_DATA_HOME` is set,
otherwise `~/.local/share/<identifier>`; caches under
`$XDG_CACHE_HOME` or `~/.cache`.

Never write into the user's home root, the install directory, or
`dist/`/`build/` (Rivet-owned generated roots). The application identifier
from `rivet.rktd` is the conventional directory name.

Products should keep one small, testable resolver for that directory and
honor an environment-variable override (for example `<PREFIX>_DATA_DIR`),
so acceptance suites can inject disposable roots instead of touching a
developer's real profile — see [product acceptance evidence](product-acceptance.md)
and the native-UI launch orchestration, both of which rely on that hook.
Write files atomically (write to a temporary file in the same directory,
then rename), and load defensively: a truncated or corrupt file is a reason
to fall back to defaults (and surface a diagnostic), not to crash on
startup.

## User-visible strings: shared/i18n

Projects that show user-facing strings keep one flat single source:
Expand Down
3 changes: 3 additions & 0 deletions docs/native-ui-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ reaping the application around it. A proven shape for that runner:
1. `raco rivet package` produces the development distributable; locate the
newest bundle under `dist/` and its executable with
`plutil -extract CFBundleExecutable raw` on the bundle's `Info.plist`.
By default `package` also launches the artifact once as a launch smoke —
pass `--skip-launch-smoke` when the suite itself owns launching, so the
run stays hermetic.
2. Launch the executable in the background with a disposable data root in the
environment (the application's own override hook — never the developer's
real profile), and take `$!` as the pid for the driver.
Expand Down
31 changes: 24 additions & 7 deletions docs/product-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ The supported suite kinds are:
Suite ids are unique symbols. Platforms are a non-empty subset of `windows`,
`macos`, and `linux`. A command is a non-empty list of argv strings; shell
syntax, redirection, pipes, and command interpolation are not interpreted.
Every suite command runs with the project root as its current directory, so
paths in a suite command are relative to the project.
Timeouts are positive integers capped at two hours. Evidence entries are file
paths below `.rivet/acceptance/`. Rivet removes each declared file before the
suite starts and then requires the suite to recreate it, so a stale artifact
Expand Down Expand Up @@ -111,19 +113,34 @@ ordinary Racket module: requiring it runs the `define-*` declarations, and
process can exercise a real embedded-backend round trip:

1. Instantiate the backend module under test (declarations register when the
module is instantiated); use a fresh namespace when a test needs a
"restarted" backend, and take `record-ref` from that same namespace.
module is instantiated). A test that needs a "restarted" backend must
re-instantiate the module under a fresh namespace, take `serve` (and any
`record-ref`) from that same namespace, and require the module by file
path — a quoted symbol resolves only in the namespace that already knows it:

```racket
(define ns (make-base-empty-namespace))
(parameterize ([current-namespace ns])
(namespace-require/copy `(file ,(path->string backend-path))))
(define serve-proc
(parameterize ([current-namespace ns]) (eval #'serve)))
```
2. Build a pipe pair, run `(serve in out)` on a thread under a custodian, and
remember that `serve` blocks until the session ends; closing the client's
output port ends it.
3. The backend writes the hello frame at startup, before it reads anything,
so the first `read-frame` on the client side observes it.
4. Speak RVT1 with `rivet/protocol`: `write-frame`, `read-frame`, and
`encode-value`/`decode-value` are public. There is no separate in-process
RPC entry point — the protocol boundary is the contract under test.
4. Speak RVT1 with `rivet/protocol`, which publicly exports the full testing
surface: `protocol-version`, the `frame` struct and its accessors,
`message:hello`, `message:request`, `message:response`, `message:error`,
`message:event`, `message:cancel`, `message:shutdown`, `write-frame`,
`read-frame`, `encode-value`, `decode-value`, and the limit constants.
There is no separate in-process RPC entry point — the protocol boundary is
the contract under test.
5. Assert wire-visible outcomes: the response payload for an accepted call,
an error frame for a rejected input, and the reserved `$state` Event that
a State mutation emits before the response frame.
an error frame for a rejected input (its payload is an encoded String
value), and the reserved `$state` Event that a State mutation emits before
the response frame.

```racket
(require rivet/backend
Expand Down
5 changes: 3 additions & 2 deletions docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Void procedures return the Null/Void value. `$state/get` and `$state/set` return

## Error

Errors associated with requests reuse the request id. The v1 error payload is a UTF-8 String containing a human-readable message.
Errors associated with requests reuse the request id. The v1 error payload is a UTF-8 String containing a human-readable message. On the wire that string is an RVT1-encoded String value — clients decode it with the value decoder (`decode-value` on Racket, the generated client's value decoding on native sides) and receive a plain string; the payload bytes are not the raw UTF-8 text. The Swift runtime surfaces a backend Error frame as `ClientError.backend(String)` from the generated calls, carrying that decoded message (with a generic fallback when the payload is not a String value).

The Racket backend bounds generated Error diagnostics to 4096 Unicode characters. Longer exception or rejection messages are truncated and end with `[truncated]`. For admitted requests, Rivet encodes this bounded Error payload before competing with cancellation for terminal-response ownership, so an Error serialization failure cannot silently consume the request without sending a terminal frame.

Expand Down Expand Up @@ -178,7 +178,8 @@ The Racket backend represents schema values as ordinary Racket data, validated a
- `(Optional T)` uses `(void)` as the absent value — it is the only Racket value that encodes as Optional null. `'()` and `#f` are rejected for an Optional field even though `'()` is a legal empty `(List T)`; the representation is type-directed, not value-directed.
- `define-record` generates a constructor named after the type — `(Task 1 "ship docs" 1761300000000)` for `(define-record Task ([id : Int64] [title : String] [created-at-ms : Int64]))`. Field access uses `(record-ref value 'title)` (the field name may be a symbol or a string, and is contract-checked). Records are opaque structs: `match` patterns and generated accessors do not exist, so use the constructor and `record-ref`. A record value is tied to the module instance that declared its type; a test that re-instantiates the backend module under a fresh namespace must also take `record-ref` from that namespace, because the predicate rejects values from another instance.
- `define-enum` likewise generates a constructor named after the type taking the case (`(Priority "high")`), and `enum-case` reads the case back as a symbol.
- `define-state` binds a State descriptor; `state-ref` and `state-set!` read and write it, and `state-set!` validates and pre-encodes the `$state` Event before committing. `emit-event!` publishes a typed Event from anywhere in the backend.
- `define-state` binds a State descriptor; `state-ref` and `state-set!` read and write it, and `state-set!` validates and pre-encodes the `$state` Event before committing. `emit-event!` publishes a typed Event from anywhere in the backend. Calling `state-set!` before a session exists is safe: the cell commits and no Event is emitted (there is no transport to deliver it on yet), so initialization-time writes behave like ordinary mutation.
- `define-rpc` procedures take their declared arguments. A zero-argument RPC omits the argument list: `(define-rpc (ping : String) "pong")`. Its wire request payload is the single-element list `["ping"]`.

`raco rivet build` regenerates the native clients from these declarations; the Racket forms above are the single source of truth for both sides.

Expand Down
Loading