waymaker-client/ts
Michael Netshipise 87a6171969 waymaker-client v0.1.27 — polyglot Go / TypeScript / Rust clients
Extract the waymaker client into a standalone, polyglot repo versioned in
lockstep with the waymaker server (this tag == server v0.1.27).

- proto/   vendored client-facing protos (source of truth: waymaker repo).
           internal_proxy (server-internal ProxyService + replication) is
           intentionally excluded — it is a server concern, not a client one.
- rust/    standalone crate; build.rs generates from proto/. Quorum fence
           scope + Lock auto-reacquire / fence-watch (watch/is_lost) API.
- go/      module git.awesomike.com/pub/waymaker-client/go — generated stubs
           + ergonomic wrappers (locks/streams/kv/collections/sketches/object).
- ts/      @waymaker/client — generated stubs + ergonomic wrappers.
- scripts/ sync-protos.sh (re-vendor + align VERSION) + gen-go.sh + gen-ts.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 19:20:48 +02:00
..
src waymaker-client v0.1.27 — polyglot Go / TypeScript / Rust clients 2026-06-09 19:20:48 +02:00
README.md waymaker-client v0.1.27 — polyglot Go / TypeScript / Rust clients 2026-06-09 19:20:48 +02:00
package-lock.json waymaker-client v0.1.27 — polyglot Go / TypeScript / Rust clients 2026-06-09 19:20:48 +02:00
package.json waymaker-client v0.1.27 — polyglot Go / TypeScript / Rust clients 2026-06-09 19:20:48 +02:00
tsconfig.json waymaker-client v0.1.27 — polyglot Go / TypeScript / Rust clients 2026-06-09 19:20:48 +02:00

README.md

@waymaker/client — TypeScript

Official TypeScript client for waymaker. Full-parity with the Rust client across all subsystems.

Installation

npm install @waymaker/client

Requires Node 18+ (for crypto.randomUUID, Buffer, async iterators).

Quick start

import { WaymakerClient } from "@waymaker/client";

const client = WaymakerClient.connect("localhost:8818");

// ---- locks ----
import { Scope } from "@waymaker/client";

const lock = await client.acquireLock("leader:myjob", {
  maxWaitMs: 0,        // fail immediately if contended
  leaseTtlMs: 60_000,
  scope: Scope.Local,
});

const renewal = lock.spawnRenewal(30_000);
lock.on("change", (state) => {
  if (state.lost) console.error("lock lost — fence token was", state.fenceToken);
});

try {
  // Work can outlive a single lease window; renewal keeps it alive.
  await doWork(lock.fenceToken());
} finally {
  renewal.stop();
  await lock.unlock();
}

client.close();

Multi-node (cluster)

const client = WaymakerClient.connectMulti([
  "node1:8818",
  "node2:8828",
  "node3:8838",
]);

grpc-js round-robins requests across the list and reroutes automatically when an endpoint is unreachable.

Subsystems

Locks

import { Scope } from "@waymaker/client";

// Exclusive lock
const lock = await client.acquireLock("my-resource");
await lock.unlock();

// Shared lock
const rlock = await client.acquireReadLock("my-resource");
await rlock.unlock();

// Atomic multi-lock (deadlock-free: server sorts keys)
const leases = await client.multiLock([
  { key: "a", writeLock: true },
  { key: "b", writeLock: false },
]);

// Operator introspection
const held = await client.listAcquiredLocks("leader:");

Leader-election pattern (mirrors the waymaker-ctl renew loop):

const lock = await client.acquireLock("leader:batch", {
  maxWaitMs: 0,
  leaseTtlMs: 60_000,
  scope: Scope.Local,
});

// Renewal runs independently of the work loop.
const renewal = lock.spawnRenewal(20_000);

lock.on("change", (s) => {
  if (s.lost) process.exit(1);  // give up leadership
});

try {
  await runBatch();
} finally {
  renewal.stop();
  await lock.unlock();
}

Streams

import { RetentionPolicy, DeliverPolicy } from "@waymaker/client";

const stream = await client.getOrCreateStream({
  name: "events",
  subjects: ["events.>"],
  retention: { policy: RetentionPolicy.Limits, maxAgeMs: 7 * 86_400_000 },
  replicationFactor: 3,
});

// Publish
const seq = await stream.publish("events.user.123", Buffer.from("hello"));

// Pull consumer
const consumer = await stream.getOrCreateConsumer({
  name: "processor",
  deliverPolicy: DeliverPolicy.All,
  ackPolicy: "explicit",
});
const msgs = await consumer.fetch(10);
for (const msg of msgs) {
  console.log(msg.subject, msg.data.toString());
  await msg.ack();
}

// Push consumer (async iterator)
for await (const msg of consumer.messages()) {
  await msg.ack();
}

KV

const bucket = await client.getOrCreateKv({ name: "config", maxRevisions: 5 });

await bucket.put("db.host", "localhost");
const val = await bucket.get("db.host");

// CAS
const rev = await bucket.create("lock", Buffer.from("1"));
await bucket.update("lock", Buffer.from("2"), rev);

// Watch
for await (const ev of await bucket.watch("db.host")) {
  if (ev.kind === "put") console.log("new value:", ev.value.toString());
}

Collections

// Hash
const hs = await client.createHashStore({ name: "users" });
const h = hs.hash("user:42");
await h.set("email", "alice@example.com");
console.log(await h.get("email"));

// Set
const ss = await client.createSetStore({ name: "tags" });
const s = ss.set("article:1");
await s.add(Buffer.from("typescript"));
console.log(await s.members());

// Queue
const q = await client.createQueue({ name: "jobs" });
await q.push(Buffer.from(JSON.stringify({ type: "email" })));
const item = await q.pop();

Sketches

// Bloom filter
const bloom = await client.bloomReserve("seen-ids", 1_000_000, 0.001);
await bloom.add(Buffer.from("msg-123"));
console.log(await bloom.exists(Buffer.from("msg-123"))); // true

// HyperLogLog
const hll = await client.hllReserve("unique-visitors");
await hll.add(Buffer.from("user-abc"));
console.log(await hll.count()); // approx cardinality

// Count-Min Sketch
const cms = await client.cmsReserve("event-counts", 0.001, 0.999);
await cms.add(Buffer.from("click"), 5);
console.log(await cms.count(Buffer.from("click")));

// Top-K
const topk = await client.topKReserve("hot-keys", 10);
await topk.add(Buffer.from("key-1"), 100);
console.log(await topk.list());

// t-digest
const td = await client.tdigestCreate("latency-p99", 200);
await td.add(42.5);
console.log(await td.quantile(0.99));

Object store

const store = await client.getOrCreateObjectStore({
  name: "artifacts",
  maxBytes: 10 * 1024 * 1024 * 1024, // 10 GiB
});

const info = await store.put("report.pdf", pdfBytes);
console.log(info.sha256, info.totalBytes);

const { payload } = await store.get("report.pdf");
const entries = await store.list();

Cache (stub)

The cache subsystem is wired but returns unimplemented until the first eviction policy ships server-side. The client surface is complete so callers can compile against it today:

await client.cacheAttachPolicy("my-bucket", "lru", { max_entries: "1000" });
await client.cacheDetachPolicy("my-bucket");

TypeScript notes

  • All bytes proto fields are typed as Buffer.
  • uint64 / int64 proto fields are typed as number. Values beyond Number.MAX_SAFE_INTEGER require BigInt — use Long from the long package if you need exact 64-bit arithmetic.
  • Streaming RPCs (watch, push consumers) return async iterables; iterate with for await.
  • The Lock class extends EventEmitter; subscribe with lock.on("change", handler).
  • Strict TypeScript (noImplicitAny, strictNullChecks). No any leaks through the public API surface.

Regenerating proto stubs

The generated files in src/genpb/ must not be hand-edited. Regenerate via:

bash ../scripts/gen-ts.sh

Building from source

npm install
npm run build    # emits to dist/