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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`apiKey` client option.** Sent as the `X-API-Key` header on every
request. Every endpoint still answers without one; a key raises the limits,
which matters for the history endpoints (30 days of history and 600
requests an hour with a free key, against 7 days and 60 without).

```js
const tp = new ThemeParks({ apiKey: 'your-api-key' });
```

- **History endpoints.** `tp.entity(id).history.changes(query)`,
`.daily(query)` and `.coverage()` over `GET /entity/{id}/history`,
`/history/daily` and `/history/coverage`, with `tp.raw.getEntityHistory`,
`getEntityHistoryDaily` and `getEntityHistoryCoverage` underneath. The
query is `{ date }` or `{ from, to }`, park-local days or RFC 3339
instants, and is sent through `URLSearchParams`, so an instant's `+02:00`
offset survives the trip. A `PARK` answers with `entities[]` for every
entity in it; every other type answers for itself. New exported types:
`EntityHistory`, `EntityHistoryDaily`, `EntityHistoryCoverage`,
`HistoryQuery`.

```js
const day = await tp.entity(barnstormerId).history.changes({ date: '2026-09-17' });
if (!('entities' in day)) {
for (const row of day.history) console.log(row.time, row.queue?.STANDBY?.waitTime);
}
```

The default cache keeps `coverage` for an hour and leaves `changes` and
`daily` uncached, since a range that holds today is not final.

## [8.0.0] - 2026-09-08

### Fixed
Expand Down
80 changes: 73 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Jungle Cruise 40 min
| ----------- | ----------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `baseUrl` | `string` | `https://api.themeparks.wiki/v1` | API base URL (point at a mock / staging if you need to). |
| `userAgent` | `string` | `themeparks-sdk-js/<version>` | Sent as the `User-Agent` header. Set this to identify your app. |
| `apiKey` | `string` | none | API key from api.themeparks.wiki, sent as `X-API-Key`. Optional; a key raises the limits. |
| `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation. Useful for logging, mocking, or older runtimes. |
| `timeoutMs` | `number` | `10000` | Per-request timeout in milliseconds. |
| `retry` | `Partial<RetryConfig>` | `{ max: 3, on429: true }` | Retry/backoff behavior. `max` counts retries **beyond** the initial attempt (so `3` = up to 4 total). |
Expand All @@ -63,6 +64,7 @@ Example:
```js
const tp = new ThemeParks({
userAgent: 'my-app/1.2.3 (+https://example.com)',
apiKey: 'your-api-key',
timeoutMs: 15_000,
retry: { max: 5, on429: true },
});
Expand Down Expand Up @@ -159,6 +161,68 @@ const entries = await tp.entity(mk).schedule.range(new Date('2026-05-01'), new D
console.log(`${entries.length} schedule entries`);
```

## History

Three endpoints answer what an entity did in the past. Days are park-local; the
range is one day (`date`) or `from`/`to` (days inclusive, or RFC 3339 instants
with `to` exclusive). Without a range the call means today.

```js
import { ThemeParks } from 'themeparks';

const tp = new ThemeParks({ apiKey: 'your-api-key' });
const barnstormer = tp.entity('924a3b2c-6b4b-49e5-99d3-e9dc3f2e8a48');

// Every recorded change of one day: the full live-data object per change, plus
// `time` and the `changed` paths.
const day = await barnstormer.history.changes({ date: '2026-09-17' });
if (!('entities' in day)) {
console.log(`opened ${day.opening.status}, ${day.history.length} changes`);
for (const row of day.history) {
console.log(row.time, row.status, row.queue?.STANDBY?.waitTime ?? '--', row.changed.join(','));
}
}

// One row per day: operating and down minutes, standby min/p50/p90/max/mean.
const week = await barnstormer.history.daily({ from: '2026-09-11', to: '2026-09-17' });
if (!('entities' in week)) {
for (const d of week.days) console.log(d.date, d.operatingMinutes, d.standby?.p50);
}

// Which days, and which live-data fields, are held at all.
const coverage = await barnstormer.history.coverage();
console.log(coverage.firstRecordedAt, coverage.retrievableThrough, Object.keys(coverage.kinds));
```

Sample output of the first loop:

```
2026-09-17T12:30:25Z OPERATING 5 status,queue.STANDBY.waitTime,queue.RETURN_TIME.returnStart,queue.RETURN_TIME.returnEnd
2026-09-17T12:43:48Z OPERATING 10 queue.STANDBY.waitTime
```

Three things to know before polling these:

- **A park answers for the whole park.** `changes` and `daily` on a `PARK`
return an `entities[]` array, one entry per entity with history, instead of
`history[]` / `days[]`; the range of a park is limited to one day for
`changes`. Every other entity type, a `DESTINATION` included, answers for
itself. Narrow with `'entities' in res`, as above.
- **The window and the budget depend on the key.** Anonymous callers see 7
days back and 60 history requests an hour; a free key sees 30 days and 600.
A day outside the window is a 403 `ApiError` whose
`body.error.earliestAllowedDate` names the first day you may ask for. Over
the budget is a 429 whose `Retry-After` can be most of an hour; with the
default `retry.on429` the client sleeps that long before trying again, so a
poller that would rather fail fast passes `retry: { on429: false }` and reads
`err.retryAfterMs`.
- **Today is not final.** The default cache leaves `changes` and `daily`
uncached and keeps `coverage` for an hour. A completed day never changes, so
cache it yourself for as long as you like.

`tp.raw.getEntityHistory(id, query)`, `getEntityHistoryDaily(id, query)` and
`getEntityHistoryCoverage(id)` are the underlying calls.

## Low-level escape hatch

Every ergonomic helper is built on top of `tp.raw`, which is a thin, typed 1:1 wrapper over the OpenAPI operations. Use it directly when you want the raw response shape:
Expand Down Expand Up @@ -220,13 +284,15 @@ const tp = new ThemeParks({

The default client caches `GET` responses in-memory (LRU) with sensible per-endpoint TTLs:

| Endpoint | TTL | Rationale |
| ------------------------------------- | ---------- | --------------------------------- |
| `GET /destinations` | 1 hour | Directory rarely changes. |
| `GET /entity/{id}` | 1 hour | Entity metadata is static. |
| `GET /entity/{id}/children` | 1 hour | Park topology is stable. |
| `GET /entity/{id}/schedule[/yyyy/mm]` | 5 minutes | Schedules update but not rapidly. |
| `GET /entity/{id}/live` | 0 (bypass) | Live data is always fetched. |
| Endpoint | TTL | Rationale |
| ------------------------------------- | ---------- | ----------------------------------- |
| `GET /destinations` | 1 hour | Directory rarely changes. |
| `GET /entity/{id}` | 1 hour | Entity metadata is static. |
| `GET /entity/{id}/children` | 1 hour | Park topology is stable. |
| `GET /entity/{id}/schedule[/yyyy/mm]` | 5 minutes | Schedules update but not rapidly. |
| `GET /entity/{id}/live` | 0 (bypass) | Live data is always fetched. |
| `GET /entity/{id}/history/coverage` | 1 hour | Whole days; changes once a day. |
| `GET /entity/{id}/history[/daily]` | 0 (bypass) | A range holding today is not final. |

### Disable caching

Expand Down
Loading
Loading