waymaker-client/ts/README.md

262 lines
6.1 KiB
Markdown

# @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/
```