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>
|
||
|---|---|---|
| .. | ||
| src | ||
| README.md | ||
| package-lock.json | ||
| package.json | ||
| tsconfig.json | ||
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
bytesproto fields are typed asBuffer. uint64/int64proto fields are typed asnumber. Values beyondNumber.MAX_SAFE_INTEGERrequireBigInt— useLongfrom thelongpackage if you need exact 64-bit arithmetic.- Streaming RPCs (
watch, push consumers) return async iterables; iterate withfor await. - The
Lockclass extendsEventEmitter; subscribe withlock.on("change", handler). - Strict TypeScript (
noImplicitAny,strictNullChecks). Noanyleaks 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/