Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,16 @@ console.log(deliveries.deliveries);
Webhook mutations are not retried automatically because the API does not yet accept webhook idempotency keys; this avoids duplicate endpoint creation or secret rotation after an ambiguous timeout. Webhook destination reachability and DNS safety are checked by the API. A client-side HTTPS check is not an authorization to deliver to a destination.

The client retries network errors, 429, and transient 5xx statuses at most twice by default. It enforces a 10 second request timeout and returns `TidelightApiError` with status, code, and request ID.

## Run the asynchronous research example

From a mother-repository checkout:

```sh
npm ci --prefix sdk/typescript
npm test --prefix sdk/typescript
export TIDELIGHT_API_KEY='tl_test_…'
node sdk/typescript/examples/research.mjs NVDA
```

The example calls the SDK company snapshot job flow and prints a result only after the API reports `complete`. It preserves job and request IDs and never prints the key. CI injects a mock transport and makes no network calls. A live run requires `research:run` and consumes project quota (10 jobs per rolling hour). This package is not available from npm; run from the repository source.
5 changes: 5 additions & 0 deletions examples/research-flow.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function runCompanyResearch(client, { ticker, idempotencyKey, intervalMs = 2_000, timeoutMs = 120_000 }) {
const accepted = await client.startCompanyResearch(ticker, idempotencyKey);
const job = await client.waitForResearchJob(accepted.job_id, { intervalMs, timeoutMs });
return { accepted, job };
}
28 changes: 28 additions & 0 deletions examples/research.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { randomUUID } from "node:crypto";
import { TidelightApiError, TidelightClient } from "../dist/index.js";
import { runCompanyResearch } from "./research-flow.mjs";

const apiKey = process.env.TIDELIGHT_API_KEY;
if (!apiKey) {
console.error("Set TIDELIGHT_API_KEY in this server-side shell.");
process.exit(2);
}
const ticker = process.argv[2] ?? process.env.TIDELIGHT_TICKER ?? "NVDA";
try {
const { accepted, job } = await runCompanyResearch(new TidelightClient({ apiKey }), { ticker, idempotencyKey: randomUUID() });
const output = {
accepted: { job_id: accepted.job_id, status: accepted.status, duplicate: accepted.duplicate, request_id: accepted.request_id },
job: { job_id: job.job_id, ticker: job.ticker, status: job.status, request_id: job.request_id },
...(job.status === "complete" ? { result: job.result } : {}),
...(job.error ? { error: { code: job.error.code } } : {}),
};
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
if (job.status !== "complete") process.exitCode = 1;
} catch (error) {
if (error instanceof TidelightApiError) {
console.error(JSON.stringify({ error: { status: error.status, code: error.code, request_id: error.requestId } }));
} else {
console.error("Research request failed. Inspect the server's safe error telemetry.");
}
process.exitCode = 1;
}
36 changes: 36 additions & 0 deletions tests/research-example.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import test from "node:test";
import assert from "node:assert/strict";
import { TidelightClient } from "../dist/index.js";
import { runCompanyResearch } from "../examples/research-flow.mjs";
const key = `tl_test_${"a".repeat(43)}`;
const jobId = "01234567-89ab-4cde-8fab-0123456789ab";
const job = (status, result = null, error = null) => ({
job_id: jobId, ticker: "NVDA", status, attempts: 1, max_attempts: 3, result, error,
created_at: "2026-10-16T00:00:00Z", started_at: null, completed_at: null, retry_at: null, request_id: "research-result",
});
test("source example exercises the actual SDK idempotent research flow", async () => {
const requests = [];
const client = new TidelightClient({ apiKey: key, fetch: async (url, init) => {
requests.push({ url: String(url), init });
if (init.method === "POST") return Response.json({ job_id: jobId, status: "queued", duplicate: false, request_id: "research-accepted" }, { status: 202 });
return Response.json(job("complete", { source: "contract-shaped test fixture" }));
} });
const result = await runCompanyResearch(client, { ticker: "NVDA", idempotencyKey: "example-run-0001", intervalMs: 250, timeoutMs: 1_000 });
assert.equal(result.accepted.status, "queued");
assert.equal(result.job.status, "complete");
assert.deepEqual(result.job.result, { source: "contract-shaped test fixture" });
assert.equal(requests.length, 2);
assert.equal(requests[0].init.headers["Idempotency-Key"], "example-run-0001");
assert.equal(requests[0].init.headers.Authorization, `Bearer ${key}`);
assert.match(requests[0].url, /\/v1\/research\/jobs$/);
assert.match(requests[1].url, new RegExp(jobId));
});
test("source example preserves failed terminal state without inventing a result", async () => {
const client = new TidelightClient({ apiKey: key, fetch: async (_url, init) => init.method === "POST"
? Response.json({ job_id: jobId, status: "queued", duplicate: false, request_id: "accepted" }, { status: 202 })
: Response.json(job("failed", null, { code: "provider_unavailable", message: "Unavailable" })) });
const result = await runCompanyResearch(client, { ticker: "NVDA", idempotencyKey: "example-run-0002", intervalMs: 250, timeoutMs: 1_000 });
assert.equal(result.job.status, "failed");
assert.equal(result.job.result, null);
assert.equal(result.job.error.code, "provider_unavailable");
});
Loading