Files
2026-05-27 23:29:53 +01:00

8.5 KiB

Capability Spec Delta: queue-distribution-http (enterprise-queue-distribution)

Package: @suaveplan/queue-distribution-http Status: PROPOSED

Every requirement below carries a **Verified by:** line at authoring time. (See openspec/AGENTS.md Section 13.)

ADDED Requirements

Requirement: Worker communicator interface and abstract base

The package SHALL expose WorkerCommunicator with sendTask<I, O>(endpoint, task, input, signal?): Promise<O>, checkStatus(endpoint, signal?): Promise<WorkerStatus>, checkHealth(endpoint, signal?): Promise<WorkerHealth>, connect(endpoint, signal?): Promise<void>, disconnect(endpoint, signal?): Promise<void>, ping(endpoint, timeoutMs?, signal?): Promise<number>, getProtocol(): string. AbstractWorkerCommunicator SHALL provide constructor-injected defaultTimeoutMs and headers, plus protected serialize/deserialize/createTimeout/withTimeout helpers. The timeout race SHALL use an injected Clock, not setTimeout at module scope.

Scenario: withTimeout honours injected Clock

  • WHEN a FakeClock is injected and a never-resolving promise is passed to withTimeout(promise, 100)
  • THEN after advancing the clock by 100 ms, withTimeout rejects with QueueDistributionHttpError(HTTP.TIMEOUT)

Verified by: src/communicator/base/base.test.ts::"withTimeout rejects with HTTP.TIMEOUT after clock advances past deadline"


Requirement: HTTP communicator with injectable fetch and Zod-validated wire envelope

The package SHALL ship HttpWorkerCommunicator extending AbstractWorkerCommunicator. The class SHALL accept an injectable fetch function (default: globalThis.fetch). All responses SHALL be parsed against a Zod schema WorkerApiResponseSchema<T> shaped { success: boolean; data?: T; error?: { code: string; message: string; details?: unknown } }. Non-conformant responses SHALL throw QueueDistributionHttpError(HTTP.RESPONSE_MALFORMED). HTTP non-2xx OR success === false SHALL throw QueueDistributionHttpError(HTTP.PROTOCOL_ERROR) carrying the upstream error.code and error.message on cause. Network-layer fetch rejections SHALL throw QueueDistributionHttpError(HTTP.REQUEST_FAILED) with the original error on cause. The implementation SHALL forward AbortSignal to fetch. getProtocol() SHALL return "http". sendTask SHALL POST { taskId, taskMetadata, input } to <endpoint>/execute. checkStatus SHALL GET <endpoint>/status. checkHealth SHALL GET <endpoint>/health. ping SHALL GET <endpoint>/ping and return the round-trip duration via the injected Clock.

Scenario: sendTask happy path

  • WHEN an injected fetch returns { success: true, data: <O> } at <endpoint>/execute
  • THEN sendTask resolves with the typed <O> value AND the queue_dist_http.request.duration_ms histogram emits one observation

Scenario: response envelope rejection

  • WHEN an injected fetch returns { foo: 1 } (no success field)
  • THEN sendTask rejects with QueueDistributionHttpError(HTTP.RESPONSE_MALFORMED)

Scenario: protocol error carries upstream code

  • WHEN an injected fetch returns 500 with body { success: false, error: { code: "INTERNAL", message: "boom" } }
  • THEN sendTask rejects with QueueDistributionHttpError(HTTP.PROTOCOL_ERROR) and error.cause.message === "boom"

Scenario: AbortSignal forwarded to fetch

  • WHEN sendTask(endpoint, task, input, signal) is called and signal.abort() fires before fetch resolves
  • THEN the call rejects, the injected fetch sees init.signal.aborted === true, AND the error code is HTTP.REQUEST_FAILED with cause.name === "AbortError"

Verified by: src/communicator/http/http-communicator.test.ts::"sendTask resolves with typed data on success" AND src/communicator/http/http-communicator.test.ts::"rejects with HTTP.RESPONSE_MALFORMED when envelope shape is wrong" AND src/communicator/http/http-communicator.test.ts::"rejects with HTTP.PROTOCOL_ERROR and carries upstream message on cause" AND src/communicator/http/http-communicator.test.ts::"forwards AbortSignal to the injected fetch"


Requirement: RemoteWorker — connect/reconnect via @suaveplan/retry

The package SHALL ship RemoteWorker extending AbstractWorker from @suaveplan/queue-distribution. Constructor accepts endpoint, communicator: WorkerCommunicator, capacity, weight, optional id, optional logger, and RemoteWorkerOptions { healthCheckIntervalMs?, maxConnectionAttempts? }. doStart() SHALL invoke connect() and start a periodic health-check loop on the injected Clock. connect() SHALL use @suaveplan/retry exponential-backoff between attempts (initial delay 1,000 ms, factor 2, max 10,000 ms, max attempts from maxConnectionAttempts default 3). The retry SHALL NOT recurse — it MUST use a bounded loop. doStop() SHALL stop the health-check loop and disconnect the communicator; disconnect errors SHALL be logged but NOT re-thrown. doExecuteTask SHALL throw HTTP.CONNECT_FAILED when isConnected === false. Errors during communicator.sendTask SHALL be routed through handleCommunicationError(error): ping with a 5,000 ms timeout; if ping succeeds, the error propagates and the worker stays in current status; if ping fails, the worker is markUnhealthy("Unreachable"), isConnected = false, and a single non-recursive attemptConnection() is scheduled. getHealth() SHALL fetch real health from the remote worker; failures SHALL log a warning and return a default unhealthy payload. getCapabilities() SHALL return { isRemote: true, protocol, endpoint }. getType() SHALL return "remote".

Scenario: connect succeeds on first try

  • WHEN worker.start() is called and communicator.connect/checkStatus resolve with WorkerStatus.ONLINE
  • THEN worker.status === ONLINE AND the connection-attempts counter is back at 0

Scenario: connect retries with exponential backoff and exhausts

  • WHEN communicator.connect rejects on every call under maxConnectionAttempts === 3 and the test injects a FakeClock
  • THEN worker.start() rejects with QueueDistributionHttpError(HTTP.CONNECT_FAILED) AND the FakeClock recorded sleeps of 1,000 ms then 2,000 ms (no recursion stack growth)

Scenario: unreachable remote marks worker UNHEALTHY

  • WHEN sendTask throws and a subsequent ping rejects
  • THEN worker.status === UNHEALTHY, isConnected === false, AND a follow-up attemptConnection is scheduled (not awaited)

Verified by: src/worker/remote/remote.test.ts::"start resolves with ONLINE on first connect" AND src/worker/remote/remote.test.ts::"exhausts retries and throws HTTP.CONNECT_FAILED under fake clock" AND src/worker/remote/remote.test.ts::"marks worker UNHEALTHY and schedules reconnect when ping fails"


Requirement: Stable error registry for the HTTP plugin

The package SHALL register every error code through defineCodes("HTTP", [...]) from @suaveplan/error and expose QueueDistributionHttpError extends SuaveplanError<HttpErrorCode>. The exhaustive set is: REQUEST_FAILED, PROTOCOL_ERROR, TIMEOUT, RESPONSE_MALFORMED, CONNECT_FAILED, UNREACHABLE. Every code SHALL be thrown by ≥1 production call path.

Scenario: every registered code is thrown

  • WHEN the test suite iterates over every code in the registry
  • THEN a corresponding production throw site exists

Verified by: src/errors/queue-distribution-http-error.test.ts::"every registered HTTP.* code is thrown by a production call path"


Requirement: OpenTelemetry instrumentation

The package SHALL emit, via @suaveplan/opentelemetry under meter name "@suaveplan/queue-distribution-http":

  • histogram: queue_dist_http.request.duration_ms (attributes endpoint, operation: "sendTask" | "checkStatus" | "checkHealth" | "ping", outcome: "success" | "failure")
  • counter: queue_dist_http.request_total (same attributes)
  • counter: queue_dist_http.connect.attempts_total (attribute outcome: "success" | "failure" | "exhausted")
  • span: queue_dist_http.sendTask, queue_dist_http.connect

Scenario: histogram emission

  • WHEN sendTask resolves successfully under an in-memory MeterProvider
  • THEN one observation is recorded on queue_dist_http.request.duration_ms with outcome === "success" AND operation === "sendTask"

Verified by: src/communicator/http/http-communicator.spec-conformance.test.ts::"emits queue_dist_http.request.duration_ms histogram with operation and outcome attributes"