Repository navigation
feat(client): add the API client for the postcode gateway - #4
Merged
Merged
Conversation
Spec 0.2.0 adds the client contract and the fixtures that the mock server reads. Its grammar and vectors are the same as before, so the core passes them unchanged and only its SPEC_VERSION moves. CONTEXT.md takes the new terms of the spec's glossary. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The mock server answers from the spec's files, so a new observation changes data and no code. It reads them once at start and stops with the name of a file that has the wrong shape, so a broken spec change cannot serve a wrong body in silence. The package runs on Node, so the lint config lets it import Node modules, and a test pins that exception. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Client tests and the docs playground need answers in the gateway's shapes, with its keys, levels, errors and rate-limit headers. Each answer comes from a fixture, and a test checks each body that the code builds or changes against the spec's OpenAPI file. The rules follow the calls of 2 Oct 2026 where the docs disagree: a level above the key's gets 403, and an empty autocomplete query gets no answer. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A spec file that is not JSON now fails with its path and the parser message. A response must have exactly one kind field, and a fixture must have a body, so the server cannot send the text "undefined". A repeated scenario id or key fails with both places, and a scenario id must equal its file name. Status, delay, level and rate values must be whole numbers in range. Messages name the response or key entry. Folders are read in sorted order, so the map order does not depend on the file system. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The spec now plays 70 scenarios, adds the lookup-no-data scenario and changes the confidence of the reverse unit fixture to low. The search test expected the old value, so it follows the fixture. The truth check of the spec also plays each scenario as the client contract describes. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A client test sends X-Scenario-Id and X-Scenario-Run, and gets the next response of that run. A run keeps its own place, so two test runs never share a response. The server holds a connection open for a hang and closes it for a drop, as the timeout and network scenarios need, and it closes every open connection when a test ends. The command starts it on Node with no build. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A rate-limited key now shows no requests left, as the 429 response says. Reverse no longer returns a unit that lies outside the radius. Numbers in a query read as decimal text by value, so a level of 2.0 works and 0x9 does not. The OpenAPI test now compares the headers and the error code of each response with the file, and new tests pin the order of the key, path, origin, scope, level and credit checks. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A delayed response kept a timer alive after the client left or the server closed, so a process with a long delay did not exit for seconds. The delay now ends when the client closes the connection and when the server closes. A second close call now returns the first result, so two signals no longer give an unhandled rejection. A failure inside the error reply now closes the connection instead of leaving a rejection. The URL of an IPv6 host has brackets, and the host :: gives localhost. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The PHP client and the docs need the mock server without a TypeScript setup, so it ships as an image that runs the source on Node. CI builds the image and looks up the synthetic unit on each pull request, so a change cannot break the image unseen. A separate workflow publishes it with a provenance attestation, and only a release tag or a person starts it, because a pushed image is public. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Docker accepts the first connection on the published port before Node listens, and then resets it. The option --retry-connrefused does not retry a reset, so the probe failed with exit code 56. The option --retry-all-errors retries it. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
An empty HOST gave an empty string, and Node then bound every interface. The command now treats an empty HOST as unset, as it does for PORT. Two tests pin the loopback default, and the README says that the server trusts a checked spec folder. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The new spec commit makes the deadline scenario less sensitive to a slow runner, checks the postcode of each suggestion, and adds evidence marks and notes to the contract. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Every Gatepost client must raise the same error codes for the same responses of the gateway. The client package starts with PostcodeError and the map from each HTTP status to one code that spec/client.md fixes, so the later parts share one mapping. The package depends on the core, which parses each postcode before a request goes out, and an ADR says why that dependency is the only one. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A reverse result can come without a radius, so radiusM may be null. The client does not parse the area and the district of a reverse result, so both are the text that the gateway sent. A new gateway can send a lookup status that the client does not know, and the client keeps it as text, so the status type is text and not a closed list. Smaller fixes: the doc of timeoutMs gives the longer wait of autocomplete, the smoke script reads the spec version from the spec folder, the decision record no longer cites private planning work, and the error tests cover more status values. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The client checks its input before it sends anything, so a code that does not parse, a level, coordinate or radius out of range, or an autocomplete query that is empty or too long, never reaches the gateway. Numbers in a query use the shortest decimal form, so every client sends the same query for the same input. The readers narrow each response and read only the fields that they know, because NIPOST's API is at version 0.1 and new fields will come. A part of a reply that is missing or of the wrong type raises unexpected_response. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The gateway can stall, as it did for an empty autocomplete query, so each attempt has a timeout, and a cancelled call frees its place in the queue at once. A 502, 503 or 504, a lost connection and a timeout get a retry. A 429 waits only for a short Retry-After, and so does a 502, 503 or 504 that carries a valid one. The queue keeps a client at four requests at a time. A call also has a total deadline, so a long Retry-After ends it with the last error instead of a late retry. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
PostcodeClient joins the parts, and adds the shared calls and the cache that spec/client.md asks for. A caller can leave a shared call, and the request stops when the last caller leaves, so a field that cancels each keystroke never fills the queue. Every contract scenario of the spec runs against the mock server in the same process, so this client and the PHP client prove the same behaviour. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The reader dropped an item with no text in code and returned the rest. If the gateway renamed the field, a caller got an empty list and no error. The spec says the client never returns a guessed empty value, so the reader now raises unexpected_response, and the message tells the caller what to do. A radius or distance of Infinity counts as missing or broken, as any other non-finite number does. Dead code in the decimal writer is gone. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Five faults passed the tests: a timer that is never cleared, a queue that keeps a cancelled task in line, a queue that starts the newest task first, a wait that ignores a cancel, and a timer that stops before the body arrives. Each fault now fails a test. A call whose signal aborts during an attempt now ends at once and does not wait before it gives up. The test of the deadline now ends exactly on it. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Autocomplete used the 8 s timeout of a lookup, and the spec gives it 15 s. The cache kept every result until its key came again, so a server that asks about many coordinates grew without limit. It now drops expired results, keeps at most 1000, and refuses a result that a clearCache call overtook. The total deadline no longer loops once for each retry, so a very large maxRetries does not hang the first call. Tests now fail for a share key that ignores the level, and for a shared request that stops when one caller leaves. A scenario gets a time limit from its own waits, and an empty spec folder fails. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
DOC-3 asks that every code example runs in CI. The examples of the client call the gateway, so their tests send each call to the mock server instead, and refuse to run without it. The example runner of the core now takes the modules that an example imports, so both packages share one runner and one rule for the results that an example shows. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The README says that none of the client's error messages holds the caller's key, and no test checked it. The new test makes a client with a key, triggers every error code through the public calls, and reads the name, message, code fields, stack and cause chain of each error. It also covers the secret key refusal and the option checks. A key planted in the timeout message and in a wrapped cause made it fail first. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The roadmap asks for a nightly check against NIPOST's staging gateway with the test key, so a change of the gateway shows up within a day. The contract scenarios need the scripted answers of the mock server, so the job makes three plain calls that hold on any gateway. It makes no call until NIPOST gives a staging address, and it prints no response. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The key-secrecy test held a made-up key in one line, and the secrets job of CI read it as a real key. The test now builds the key at run time, so the tree holds no such line. History cannot change, so the fingerprint of the old finding goes into .gitleaksignore. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A gateway that echoes the API key as its error code put the key into the message of the error. No message may hold the key, so the message now names the status only. The code stays in the apiCode field. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The README named nine error codes and the client has ten, so a reader who handled each listed code left out a 200 reply with a broken body. A test now compares the list with the PostcodeErrorCode type. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Dropping lng from the reverse key, or keying autocomplete on the raw text, passed every test. The new tests send two points that differ only in lng or lat, and two spellings of one text. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The client tried every cut of the typed text and took the first that parsed at the segment precision. A value of the wrong length then gave a postcode that held typed characters of the active segment, and a short typed text gave null where the segments before it and the value parse. The client now makes one cut at the segment start and checks the precision, as the PHP client does. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A fork with Actions on ran the nightly job and used minutes, so the job now runs only in the gatepost-dev/js repository. A failed assertion also printed the values that the gateway sent. Each check is now a boolean, so a failure prints none. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The record carried a date later than every commit of the branch. It now has the date on which the decision was made. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A caller that changes a result also changes what the next caller of the cache gets. The README now says so, because a copy of each result would cost bundle size. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
An empty apiKey sent an empty X-API-Key header, which is no key. A caller who reads the key from an unset variable now gets an error when the client is made. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
A fault that removed the check of the abort signal passed every test, because the fake transport always rejected with the reason of the signal. A real transport rejects with its own AbortError after the timer fires. The new test checks that the call still ends with a timeout and that autocomplete does not retry it. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
Date.parse read any text with a letter, so 'soon 2027' gave a wait of months, and it refused the asctime form. The client now accepts the three HTTP-date forms of RFC 9110 and no other text. A two-digit year of the RFC 850 form follows Date.parse, and a date such as 31 Nov rolls over. retryAfterMs was set for every status. It now holds a wait for 429, 502, 503 and 504 only, as the spec says. The change adds bytes, so the gateway detail of unexpected_response and the advice texts are shorter, and the client keeps the bundle under 5 kB. The header is no longer trimmed, because the platform has trimmed it already. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
MobileMage
marked this pull request as ready for review
October 3, 2026 03:05
An empty apiKey is a configuration mistake, like a bad timeoutMs, maxRetries or cacheTtlMs. A caller who reads the key from an unset variable made the mistake in code, so the constructor now throws a RangeError, as it does for the other bad options. It no longer throws a PostcodeError with the code invalid_input. That code is for a bad input of a call, not for a bad option of the client. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The client sat a few bytes under its 5 KB limit, and some error messages had been cut short to fit. The owner chose 6 KB, which leaves room for the next feature without shorter messages. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
The mock server branch is merged as one commit on main. This brings that commit in, keeps the client's versions of AGENTS.md, README.md and the lockfile, which hold every change of main, and points the spec submodule at the spec release v0.2.0. Signed-off-by: MobileMage <aidelojejoshua@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@gatepost/client calls NIPOST's postcode gateway: lookup, reverse and
autocomplete, with the results, errors and limits that the spec's client
contract sets. It retries what can succeed, honours Retry-After, keeps at
most 4 requests in flight, shares calls for the same postcode and keeps an
optional cache. Its only runtime dependency is @gatepost/core.
Every contract scenario runs against the mock gateway server in the same
process, and every README and doc example runs too. A nightly workflow
checks the staging gateway once its address and key exist; until then it
makes no call.
This change is larger than the 400 lines that GIT-5 suggests, because it
adds a package. Most of it is tests.
Signed-off-by: MobileMage aidelojejoshua@gmail.com