262 lines
6.1 KiB
Markdown
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/
|
|
```
|