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. (Seeopenspec/AGENTS.mdSection 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
FakeClockis injected and a never-resolving promise is passed towithTimeout(promise, 100) - THEN after advancing the clock by 100 ms,
withTimeoutrejects withQueueDistributionHttpError(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
fetchreturns{ success: true, data: <O> }at<endpoint>/execute - THEN
sendTaskresolves with the typed<O>value AND thequeue_dist_http.request.duration_mshistogram emits one observation
Scenario: response envelope rejection
- WHEN an injected
fetchreturns{ foo: 1 }(nosuccessfield) - THEN
sendTaskrejects withQueueDistributionHttpError(HTTP.RESPONSE_MALFORMED)
Scenario: protocol error carries upstream code
- WHEN an injected
fetchreturns 500 with body{ success: false, error: { code: "INTERNAL", message: "boom" } } - THEN
sendTaskrejects withQueueDistributionHttpError(HTTP.PROTOCOL_ERROR)anderror.cause.message === "boom"
Scenario: AbortSignal forwarded to fetch
- WHEN
sendTask(endpoint, task, input, signal)is called andsignal.abort()fires before fetch resolves - THEN the call rejects, the injected fetch sees
init.signal.aborted === true, AND the error code isHTTP.REQUEST_FAILEDwithcause.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 andcommunicator.connect/checkStatusresolve withWorkerStatus.ONLINE - THEN
worker.status === ONLINEAND the connection-attempts counter is back at 0
Scenario: connect retries with exponential backoff and exhausts
- WHEN
communicator.connectrejects on every call undermaxConnectionAttempts === 3and the test injects aFakeClock - THEN
worker.start()rejects withQueueDistributionHttpError(HTTP.CONNECT_FAILED)AND theFakeClockrecorded sleeps of 1,000 ms then 2,000 ms (no recursion stack growth)
Scenario: unreachable remote marks worker UNHEALTHY
- WHEN
sendTaskthrows and a subsequent ping rejects - THEN
worker.status === UNHEALTHY,isConnected === false, AND a follow-upattemptConnectionis 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(attributesendpoint,operation: "sendTask" | "checkStatus" | "checkHealth" | "ping",outcome: "success" | "failure") - counter:
queue_dist_http.request_total(same attributes) - counter:
queue_dist_http.connect.attempts_total(attributeoutcome: "success" | "failure" | "exhausted") - span:
queue_dist_http.sendTask,queue_dist_http.connect
Scenario: histogram emission
- WHEN
sendTaskresolves successfully under an in-memoryMeterProvider - THEN one observation is recorded on
queue_dist_http.request.duration_mswithoutcome === "success"ANDoperation === "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"