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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ That gating follows slothlet's own rule about **who is calling**: a call made by

## ✨ What's New

### Latest: v1.2.2 (October 2026)
### Latest: v1.3.0 (October 2026)

- **Dev-tooling dependency bump ([#72](https://github.com/CLDMV/slothlet-vine/pull/72), [#75](https://github.com/CLDMV/slothlet-vine/pull/75))** — `@cldmv/fix-headers` moves from 2.1.1 to 2.2.0, so `@Last modified by` now follows content edits only and a header-only rewrite keeps the recorded editor, and `@cldmv/configs` moves from 1.2.0 to 1.2.4, whose shared config no longer forces the author fields. Neither changed any file header here. No runtime source changed and the peer ranges are the same as in v1.2.1 (`@cldmv/slothlet >=3.22.0`, optional `ws >=8.0.0`); it's a drop-in replacement for v1.2.1.
- [View full v1.2.2 Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.2.2.md)
- **Per-call requested context ([#80](https://github.com/CLDMV/slothlet-vine/pull/80))** — `link.with(context, fn)` and `grow({ context })` attach a data-only context to individual calls, such as the project a browser window has open. A new `serve({ context })` host check accepts, narrows or refuses it (a refusal is the new `VINE_CONTEXT` code), and whatever it accepts joins the call's scope under the principal's keys, write-protected, with no per-connection state on the server.
- **Opt-in and wire compatible** — a serve without a `context` check and a grow that sets none behave exactly as in v1.2.x; the frame version and the `@cldmv/slothlet >=3.22.0` peer range are unchanged. A serve with no check refuses a frame that carries a requested context instead of ignoring it.
- [View full v1.3.0 Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.3.0.md)

### Recent Releases

- **v1.2.2** (October 2026) — dev-tooling bump only: `@cldmv/fix-headers` 2.2.0 and `@cldmv/configs` 1.2.4, so a header-only rewrite keeps `@Last modified by`; no runtime change (#72, #75) ([Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.2.2.md))
- **v1.2.1** (October 2026) — dev-tooling dependency refresh in the lockfile only (`vitest` 5.0.3, `@cldmv/vitest-runner` 1.5.1, jsonv lint/format packages, `ws` for the transport tests); no runtime changes ([Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.2.1.md))
- **v1.2.0** (October 2026) — channel principal: `serve({ principal })` judges every call and subscription as a transport-authenticated identity, plus a per-call `serve({ around })` wrapper; requires `@cldmv/slothlet >=3.22.0` ([Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.2.0.md))
- **v1.1.3** (September 2026) — transports buffer frames that arrive before `grow()`/`serve()` registers a handler, so attaching after an `await` no longer loses the surface frame ([Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.1.3.md))
- **v1.1.2** (September 2026) — `@cldmv/slothlet` peer floor raised to `>=3.20.0`; no runtime changes ([Changelog](https://github.com/CLDMV/slothlet-vine/blob/master/docs/changelog/v1/v1.1.2.md))

📚 **For complete version history and detailed release notes, see the [docs/changelog/](https://github.com/CLDMV/slothlet-vine/tree/master/docs/changelog/) folder.**

Expand Down
89 changes: 77 additions & 12 deletions docs/CONFIGURATION.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/CUSTOM-TRANSPORTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Only `send` and `onMessage` are required. `close`, `onClose`, and `capabilities`

### The rules, restated as things to actually do

- **`send`/`onMessage` carry plain frame objects.** If your medium structured-clones (like `postMessage`), pass frames through untouched and declare `capabilities.structuredClone: true, codec: "none"`. If your medium only carries bytes/strings, own your encode/decode internally and declare `codec: "json"` (or whatever you use) — see `transport/websocket` for the reference byte transport.
- **`send`/`onMessage` carry plain frame objects.** If your medium structured-clones (like `postMessage`), pass frames through untouched and declare `capabilities.structuredClone: true, codec: "none"`. If your medium only carries bytes/strings, own your encode/decode internally and declare `codec: "json"` (or whatever you use) — see `transport/websocket` for the reference byte transport. Carry every key of a frame, including ones you don't recognize: frames grow optional keys without a version bump (a `call`'s requested `context` and a `surface`'s `context: true` flag, for example), and a transport that copies only the keys it knows would silently strip them.
- **`onMessage`/`onClose` are single-handler registrations — last write wins.** A second `onMessage(fn)` call replaces the first, it doesn't add a second listener.
- **A handler must never be allowed to throw into your transport.** Wrap every call to a registered handler:

Expand Down
33 changes: 25 additions & 8 deletions docs/DESIGN.md

Large diffs are not rendered by default.

23 changes: 12 additions & 11 deletions docs/ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ import { CODES, VineError, VineRemoteError } from "@cldmv/slothlet-vine";

## `VINE_*` codes

| Code | Fires when |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VINE_GONE` | The far side died — its channel closed (or a transport-detected death fired) with the call still in flight. |
| `VINE_BUDGET` | The per-call settle budget (`budgetMs`) elapsed before a terminal frame arrived. Also fires for a `grow()` handshake that never sees a `surface` frame within `handshakeMs`. |
| `VINE_CLOSED` | `link.close()` was called with the call still in flight, or a call was made after `close()`. |
| `VINE_DATA_ONLY` | A function-valued argument or return value was found in the data graph — the vine is data-only in v1. |
| `VINE_BAD_FRAME` | A frame couldn't be parsed, or an outbound frame couldn't be handed to the transport (the medium refused it — see [TRANSPORTS.md](TRANSPORTS.md)). |
| `VINE_NO_LEAF` | A call named a path that isn't in the served surface. Re-validated serve-side on **every** call — a path learned from an earlier, wider surface (or invented) can't reach a leaf this serve doesn't publish. |
| `VINE_DENIED` | The serving side's **channel principal** may not call this path — slothlet's rules said no (or could not be asked), and the leaf never ran. Produced only by a `serve()` that bound a `principal` (see [PERMISSIONS.md](PERMISSIONS.md#serving-to-an-untrusted-peer-the-channel-principal)); a trusted-transport serve never answers it. The message names the path only, never the principal. |
| `VINE_REMOTE` | `.code` on a `VineRemoteError` for a remote application error that carried no `code` of its own — or one whose code was a reserved `VINE_*` value (see below). |
| Code | Fires when |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VINE_GONE` | The far side died — its channel closed (or a transport-detected death fired) with the call still in flight. |
| `VINE_BUDGET` | The per-call settle budget (`budgetMs`) elapsed before a terminal frame arrived. Also fires for a `grow()` handshake that never sees a `surface` frame within `handshakeMs`. |
| `VINE_CLOSED` | `link.close()` was called with the call still in flight, or a call was made after `close()`. |
| `VINE_DATA_ONLY` | A function-valued argument or return value was found in the data graph — the vine is data-only in v1. Also a per-call **requested context** that is not data (not a plain object, a function anywhere, or a value structured clone refuses) — grow-side from `link.with()` / `grow({ context })` before anything is sent, serve-side for a hand-built frame. |
| `VINE_BAD_FRAME` | A frame couldn't be parsed, or an outbound frame couldn't be handed to the transport (the medium refused it — see [TRANSPORTS.md](TRANSPORTS.md)). |
| `VINE_NO_LEAF` | A call named a path that isn't in the served surface. Re-validated serve-side on **every** call — a path learned from an earlier, wider surface (or invented) can't reach a leaf this serve doesn't publish. |
| `VINE_DENIED` | The serving side's **channel principal** may not call this path — slothlet's rules said no (or could not be asked), and the leaf never ran. Produced only by a `serve()` that bound a `principal` (see [PERMISSIONS.md](PERMISSIONS.md#serving-to-an-untrusted-peer-the-channel-principal)); a trusted-transport serve never answers it. The message names the path only, never the principal. |
| `VINE_CONTEXT` | A per-call **requested context** was refused and the leaf never ran: the serve's `context` check answered something other than a plain data object, threw or rejected, or the serve has no check (see [PERMISSIONS.md](PERMISSIONS.md#per-call-requested-context)). A grow also produces it **locally** (a plain `VineError`, nothing sent) for a call carrying a requested context to a far side that does not accept one (`link.context === false`). Distinct from `VINE_DENIED` so a client can tell "this context is not yours" from "the rules refused this call"; an accepted context that the rules then refuse is `VINE_DENIED`. The message names the path only. |
| `VINE_REMOTE` | `.code` on a `VineRemoteError` for a remote application error that carried no `code` of its own — or one whose code was a reserved `VINE_*` value (see below). |

Every one of these is a `VineError` instance, so `err instanceof VineError` is the general "this came from the vine itself" check, and `err.code === CODES.GONE` (etc.) is how you branch on which one.

Expand Down Expand Up @@ -83,7 +84,7 @@ try {

Without that remap, a far side — hostile, or merely running its own vine and forwarding a failure verbatim — could send `{ name: "VineError", code: "VINE_CLOSED" }` and the resulting local error would satisfy `err instanceof VineError && err.code === CODES.CLOSED`, the documented way to detect _this_ link closing, and drive a consumer's teardown path from **across the boundary**. A link-state code describes this link's own state; it can only ever be produced locally.

The remap is blind to which `VINE_*` code arrived — including ones the far side's own `serve()` legitimately produced, like `VINE_NO_LEAF`, `VINE_DATA_ONLY` or `VINE_DENIED`. So a call the serving side's channel principal was not permitted to make surfaces grow-side as `err instanceof VineRemoteError && err.code === "VINE_REMOTE" && err.remoteCode === "VINE_DENIED"` — deliberately, because "this link is denied by the far side" is a statement about the far side, not about this link's state. Read `.remoteCode` to see what the far side actually said; read `.code` to know it came from over there:
The remap is blind to which `VINE_*` code arrived — including ones the far side's own `serve()` legitimately produced, like `VINE_NO_LEAF`, `VINE_DATA_ONLY`, `VINE_DENIED` or `VINE_CONTEXT`. So a call the serving side's channel principal was not permitted to make surfaces grow-side as `err instanceof VineRemoteError && err.code === "VINE_REMOTE" && err.remoteCode === "VINE_DENIED"` — deliberately, because "this link is denied by the far side" is a statement about the far side, not about this link's state. Read `.remoteCode` to see what the far side actually said; read `.code` to know it came from over there:

```javascript
try {
Expand Down
Loading
Loading