Skip to content

feat: feat(cluster-protocol): publish the TypeScript cluster client with full v1 parity - #801

Closed
tomdps wants to merge 1 commit into
devfrom
zeroshot/clever-bear-74
Closed

feat: feat(cluster-protocol): publish the TypeScript cluster client with full v1 parity#801
tomdps wants to merge 1 commit into
devfrom
zeroshot/clever-bear-74

Conversation

@tomdps

@tomdps tomdps commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #748

@tomdps
tomdps enabled auto-merge July 25, 2026 08:13
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds and publishes a TypeScript v1 cluster-protocol client.

  • Generates protocol wire types from the authoritative JSON schemas.
  • Adds unary RPC, multiplexed WebSocket transport, watch/log/agent-attach subscriptions, and durable watch reconnection.
  • Publishes CommonJS, ESM, and declaration builds through the package exports map.
  • Adds package, transport, parity, subscription, and protocol-golden tests.

Confidence Score: 2/5

The reconnect and queue lifecycle defects need to be fixed before merging because they can leak live sockets and subscriptions or leave consumers permanently pending.

Durable reconnects are neither serialized nor coordinated with close and do not clean up failed setup, while the bounded queue stores only one pending receiver and loses earlier concurrent waiters.

Files Needing Attention: src/cluster/durable-watch.ts, src/cluster/multiplexed-transport.ts

Important Files Changed

Filename Overview
src/cluster/durable-watch.ts Implements full-socket watch recovery, but reconnect failure, concurrent reads, and close races can leak or revive transports.
src/cluster/multiplexed-transport.ts Adds request demultiplexing and bounded subscription queues, but concurrent queue receives can permanently strand a consumer.
src/cluster/websocket-transport.ts Binds the multiplexed transport to browser-compatible and Node WebSocket implementations.
src/cluster/subscription-stream.ts Adds common cursorless subscription establishment, notification parsing, iteration, and cancellation.
src/cluster/watch-subscription.ts Adds cursor-aware watch subscriptions, deduplication, and same-transport reconnection.
src/cluster/cluster-client.ts Adds typed unary cluster methods with response validation and AbortSignal cancellation.
package.json Publishes the cluster subpath and integrates generation, build, and package checks into release workflows.

Sequence Diagram

sequenceDiagram
  participant Consumer
  participant DurableWatch
  participant OldSocket
  participant FreshSocket
  Consumer->>DurableWatch: next()
  OldSocket-->>DurableWatch: stream ended
  DurableWatch->>FreshSocket: connect()
  DurableWatch->>FreshSocket: "get({})"
  DurableWatch->>FreshSocket: watch(runId, fromCursor)
  DurableWatch->>OldSocket: close()
  DurableWatch-->>Consumer: resumed event stream
Loading

Reviews (1): Last reviewed commit: "feat: implement #748 - feat(cluster-prot..." | Re-trigger Greptile

Comment on lines +72 to +77
const freshTransport = await WebSocketTransport.connect(this.url, this.options);
await new ClusterClient(freshTransport).get({});
const { stream } = await new WatchSubscriptionClient(freshTransport).watchWithDedup(
{ runId, fromCursor },
dedup
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Failed reconnect leaks transport

When the fresh WebSocket connects but get() or watchWithDedup() rejects, this method propagates the error without closing freshTransport, causing repeated reconnect failures to leak sockets and any subscription established before the failure.

Comment on lines +79 to +81
this.transport.close();
this.transport = freshTransport;
this.stream = stream;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reconnect revives closed client

When close() completes while reconnect setup is awaiting connection, snapshot retrieval, or watch establishment, this code unconditionally installs the fresh transport afterward, leaving a closed client with a live WebSocket and server-side subscription that it will not consume or close.

Comment on lines +61 to +63
if (outcome !== null) return outcome;
if (this.closing) return null;
await this.reconnectFullSocket();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Concurrent reads duplicate reconnects

When two consumers call next() after the old queue has ended, both calls start reconnectFullSocket() without sharing an in-flight attempt. Each opens a socket and subscription, and the later assignment overwrites the earlier transport, leaving the first reconnect's socket and subscription unowned.

Comment on lines +52 to +54
recv(): Promise<string | null> {
if (this.buffer.length > 0) return Promise.resolve(this.buffer.shift() ?? null);
if (this.closed) return Promise.resolve(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Concurrent receives lose waiter

When a subscription stream receives two concurrent next() calls while its queue is empty, the second recv() overwrites resolveWaiting. A later event or queue shutdown resolves only the second promise, leaving the first consumer permanently pending.

@tomdps
tomdps disabled auto-merge July 25, 2026 08:19
@tomdps

tomdps commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

External review found four unresolved P1 lifecycle races: failed reconnect leaks the fresh transport, close racing reconnect can revive a closed durable watch, concurrent reads spawn duplicate reconnects and orphan a transport, and the queue's single waiting receiver leaves an earlier concurrent next() pending forever. Closing this attempt; the replacement contract must serialize reconnect, coordinate close, clean failed setup, and support/reject concurrent receivers deterministically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant