rest
@noy-db/to-rest·pnpm add @noy-db/to-restTalks to an@noy-db/in-restrelay over HTTP.
| Shape | record |
| Atomic CAS | ✓ (delegated to the server's store) |
| Locality | remote |
| Speed | network-bound |
| Persistence | durable (whatever the server's store provides) |
What it does
Sends encrypted envelopes to an HTTP endpoint running @noy-db/in-rest. Every store method POSTs { method, args } to {baseUrl}/rpc; the relay forwards the call to its own backing store and returns the result unchanged.
Encryption stays on this side. The client encrypts before the request and decrypts after the response, so the server handles ciphertext only — the same trust model as any other to-* store, reached over a network hop instead of a disk or an SDK.
import { createNoydb } from '@noy-db/hub'
import { toRest } from '@noy-db/to-rest'
const db = await createNoydb({
store: toRest({
baseUrl: 'https://relay.example.com/api/noydb',
headers: { authorization: `Bearer ${API_KEY}` },
}),
user: 'alice',
secret: 'a-strong-secret',
})
What it pairs with
The server half is @noy-db/in-rest's createRestHandler, which needs >= 0.6.0-pre.0. Earlier in-rest versions exposed a different HTTP surface — one that unlocked vaults server-side — which has been removed. There is no package dependency between the two; the coupling is the wire protocol.
Notes
ConflictErroris re-hydrated from any409whose error body is namedConflictError, so compare-and-set semantics survive the wire hop. Match it withisConflictError, neverinstanceof— the error is constructed insideto-rest's copy of@noy-db/hub/to, which need not be your caller's copy.Upgrade to0.7.0-pre.1or later. Through0.7.0-pre.0the re-hydration also requiredtypeof version === 'number', so a409that omitted the field fell through to a genericError— a lost CAS race arriving as something no retry loop can recognize.0.7.0-pre.1keys on the name alone.server sends 0.7.0-pre.00.7.0-pre.1409,name: 'ConflictError', noversionError—isConflictErrorfalseConflictError,.versionNaN409,name: 'ConflictError',version: 3ConflictError,.version3ConflictError,.version3409named anything elseErrorError.versionisNaNon any conflict from a current relay.in-reststopped sending the field in0.7.0-pre.7— it disclosed another writer's progress counter (noy-db#1218, closed) — so its 409 body is now{ name, message }and nothing else.NaNis deliberate, chosen over a-1sentinel so it cannot masquerade as a real stored counter. Test withNumber.isNaN(err.version)before using it and treat a wire conflict as conflict, position unknown: re-read and re-apply rather than reasoning from the number.ConflictErrorraised by a local store still carries a real version; only the wire hop drops it.401/403/501/5xxmap to auth, capability, not-implemented and server errors respectively — a client can branch on them without parsing prose.- Trailing optional arguments (
expectedVersion,cursor,limit) are trimmed before serialization: JSON has noundefined, and anullmust not reach the server looking like a real value. - Atomicity is whatever the server's backing store offers — the relay does not add or remove guarantees.
- Factory:
toRest(options: RestStoreOptions).
Store-locator descriptor
See store-locator descriptors for the general model.
kind: 'rest', class: 'cloud'. to-rest's direct options violate the
credentialless rule twice — fetch is a function, and headers is where a
static Authorization lives — so they're partitioned rather than forwarded:
address is { baseUrl }, options is { timeoutMs? }, and the
device-local fetch plus non-auth headers (tenant routing, tracing) ride
binding. Auth rides RestStoreOptions.credentials, a kind: 'token'StoreCredentialSource that becomes Authorization: Bearer <token>. Header
assembly happens per request, so an expiring token refreshes without
rebuilding the store — the source is invoked once per operation, and
expiresAt is not consulted (unlike to-webdav/to-turso, which cache
within a refresh-skew window).
import { restStoreDescriptor, registerRestStore } from '@noy-db/to-rest'
const descriptor = restStoreDescriptor({ baseUrl: 'https://relay.example.com/api/noydb' })
// later: locator.resolve(descriptor, { credentials: async () => ({ kind: 'token', token }) })
See also
- to-* family
- in-rest — the server half