Skip to content

Write Methods

WarmHub has one write path: every mutation lands through the same operation pipeline. The TypeScript SDK gives you two ways to submit operations:

  • client.commit.apply(...) for call sites that already have operation arrays.
  • OperationBuilder for call sites that benefit from incremental construction and local validation.

Both submit add, revise, and retract operations through the same backend stream-append surface and return the same per-operation result shape.

NeedPrefer
Submit a small operation array directlyclient.commit.apply(...)
Build operations across several branches or helper functionsOperationBuilder
Run client-side preflight checks before any server call (and shape-data validation when constructed with { shapes })OperationBuilder
Preserve a raw operation payload from another systemclient.commit.apply(...)
Chain add, revise, and retract calls fluentlyOperationBuilder
Resume caller-managed stream chunksEither, with advanced stream options
await client.commit.apply('acme', 'world', 'seed cave', [
{
operation: 'add',
name: 'Location/cave',
data: { x: 0, y: 0 },
},
])
import { OperationBuilder } from '@warmhub/sdk-ts'
const builder = new OperationBuilder()
builder.add({ name: 'Location/cave', data: { x: 0, y: 0 } })
builder.add({ name: 'Location/forest', data: { x: 5, y: 3 } })
const check = builder.validate()
if (!check.valid) {
throw new Error(check.errors.map((e) => e.message).join('; '))
}
await builder.commit({
client,
orgName: 'acme',
repoName: 'world',
message: 'seed locations',
})

The builder has no .build() step and is not itself a promise — await builder does nothing. builder.commit({...}) is the only finalizer; it validates, submits, and seals the builder so calling builder.commit(...) a second time throws.

Operation is a discriminated union over AddOperation, ReviseOperation, and RetractOperation, keyed on the operation field. When the array is passed inline to client.commit.apply, the parameter type narrows the literal for you and the call typechecks with no extra ceremony.

When you bind the array to a variable first without a type annotation, TypeScript widens operation: "add" to operation: string, and the variable no longer assigns to the Operation[] parameter. Two equivalent fixes — pick whichever fits the call site:

import type { Operation } from "@warmhub/sdk-ts";
// 1. Annotate the variable — contextually typed by the annotation.
const operations: Operation[] = [
{ operation: "add", kind: "thing", name: "Sensor/temp-1", data: { x: 1 } },
{ operation: "revise", name: "Sensor/temp-1", data: { x: 2 } },
];
// 2. Or `satisfies` — preserves the inferred literal types instead of
// widening them to `Operation`.
const operations2 = [
{ operation: "add", kind: "thing", name: "Sensor/temp-1", data: { x: 1 } },
{ operation: "revise", name: "Sensor/temp-1", data: { x: 2 } },
] satisfies Operation[];
await client.commit.apply("acme", "world", "seed", operations);

as const works too, at the cost of marking the whole array readonly.

For add operations, WarmHub can infer the operation kind from the input:

  • about present -> assertion
  • type and members present -> collection
  • one-segment name (e.g. game-state) -> thing
  • two-segment Shape/name -> thing
  • three or more segments -> assertion

Shape adds always require explicit kind: 'shape' — a bare shape name (e.g. Player) is otherwise inferred as a thing and rejected by preflight as a thing-path violation. Use kind: 'thing' for hierarchical thing names such as GameState/round-1/state if you need to keep them on the thing path despite the segment count.

Wref shape constraints are enforced server-side. OperationBuilder validates field types and most local constraints, but it cannot prove that a referenced thing belongs to the required shape until the operation reaches the server.

revise accepts an optional expectedVersion — the write applies only if the target (thing, shape, or assertion) is still at that version, otherwise it is rejected with a CONFLICT (details.reason: "expected_version_mismatch"). Use it for read-modify-write safety when you don’t need to hold an exclusive lease.

revise and retract operations accept an optional leaseId to write under a read lease acquired with client.thing.getWithLease — a leased read requires write access and is never an anonymous read. The field is per-operation, so the client.commit.apply signature is unchanged; add is never lease-gated (a new thing has no prior version to lease).

const leased = await client.thing.getWithLease("acme", "world", "Player/alice", { ttlMs: 5000 });
await client.commit.apply("acme", "world", "update score", [
{ operation: "revise", name: "Player/alice", data: { score: 2 }, leaseId: leased.lease.id },
]);
// The lease auto-releases on a successful or no-op write. To bail out without writing,
// call client.thing.releaseLease("acme", "world", "Player/alice", leased.lease.id).

A successful (or no-op) write auto-releases the lease. If the lease has already expired, the write runs as an ordinary write — the same path you would take without a lease, following the usual version-conflict rules. But if another caller still holds the lease and your leaseId doesn’t match it, the write is rejected with LEASE_UNAVAILABLE.

Submissions that apply at least one operation return operation results in input order. Mixed results include partial: true, statusCounts, and per-operation error objects for failed entries. If every operation fails, client.commit.apply(...) and OperationBuilder.commit(...) reject with AllStreamOperationsFailedError; inspect error.result or error.operations for the same per-operation failure data.

Warnings are informational. A result can still be applied or no-op’d when the submitted data includes undeclared top-level shape fields. The warning lists field names and reports truncation when the list is capped.

There is no commitId field. Version histories are the audit source; use client.thing.history(...) or client.shape.history(...) when you need to inspect what changed over time.