# @waymaker/client — TypeScript Official TypeScript client for [waymaker](https://git.awesomike.com/dev/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 ```ts 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) ```ts 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 ```ts 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): ```ts 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 ```ts 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 ```ts 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 ```ts // 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 ```ts // 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 ```ts 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: ```ts 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/ ```