All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
entity(id).history.span(),.days()and.changeRows()— the loop above the three history calls.const history = tp.entity(DISNEYLAND).history; const span = await history.span(); for await (const { entityId, row } of history.days({ from: span.archiveFrom, to: span.retrievableThrough, })) { // ... }
span()returnsarchiveFrom,recordedToandretrievableThroughin one shape. The underlying coverage documents do not: a park nests them undersummary, an entity carries them at the top level under different names, so without this every caller writes that branch first.retrievableThroughis the end date to bound a backfill by, because it is what the key may read rather than what the archive holds.days()pages until the server stops offering anext, following that URL verbatim, and yields{ entityId, row }as rows arrive rather than collecting them. A park's daily call is the one paged call in the family, so without this a park backfill silently stopped at the first 31 days.- Both flatten a park envelope and an entity envelope to the same stream, so
a caller writes one loop and does not branch on
'entities' in res. BudgetExhaustedError(aRateLimitError) is thrown when the history budget is spent and the server asks for longer thanmaxWaitMs(120000 by default). It carriesretryAfterMs, so a backfill can checkpoint and resume rather than hold a process open for most of an hour.
-
examples/backfill.mjs— a complete backfill with resume and NDJSON or CSV output. It pulled Disneyland Resort's whole daily archive, 98,452 rows, in one run.
-
A 429 could park the client for hours. The transport honoured any
Retry-Afterup toretry.maxtimes. That is right for a REST 429, which asks for seconds, and wrong for a history 429: that budget is hourly, so a spent one can ask for most of an hour, and three of those is roughly two and a half hours of a silent process.RetryConfiggainsmaxRetryAfterMs(120000 by default): past it the client does not sleep at all and throwsRateLimitErrorwithretryAfterMsset. -
EntityHistoryCoveragewas missing the park shape./entity/{id}/history/coverageanswers a PARK withHistoryParkCoverageDocument, the same way/historyand/history/dailydo, and the type named onlyHistoryCoverageDocument. The two do not overlap where it counts: a park carriessummaryandfields, an entity carriesfirstRecordedAt,lastRecordedAtandkinds. A TypeScript user read.kindsoff a park's coverage, gotundefinedat runtime, and the compiler said nothing. The fixture that covered this was hand-written in the entity shape and named after a park, so it agreed with the code for the same reason the code was wrong; both coverage fixtures are now captured from production, and the live smoke test asserts the park shape it actually gets. -
The user agent announced the wrong version.
PACKAGE_VERSIONwas still7.0.0-alpha.0in a package at8.0.0, so every request announced a version a major old and nothing failed. A gate test now asserts theUser-Agentthe server actually receives carries the versionpackage.jsondeclares, so forgetting the bump is a red test rather than a quiet lie in a header. -
apiKeyclient option. Sent as theX-API-Keyheader 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).const tp = new ThemeParks({ apiKey: 'your-api-key' });
-
History endpoints.
tp.entity(id).history.changes(query),.daily(query)and.coverage()overGET /entity/{id}/history,/history/dailyand/history/coverage, withtp.raw.getEntityHistory,getEntityHistoryDailyandgetEntityHistoryCoverageunderneath. The query is{ date }or{ from, to }, park-local days or RFC 3339 instants, and is sent throughURLSearchParams, so an instant's+02:00offset survives the trip. APARKanswers withentities[]for every entity in it; every other type answers for itself. New exported types:EntityHistory,EntityHistoryDaily,EntityHistoryCoverage,HistoryQuery.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
coveragefor an hour and leaveschangesanddailyuncached, since a range that holds today is not final.
-
Schedule entries now expose
purchases, andtypeis a union again. The upstream spec described a park's schedule two different ways: precisely when nested under a destination, loosely when fetched directly. The direct path is the one this client uses, sopurchaseswas invisible andtypewas a barestring.Magic Kingdom served 26 of 79 upcoming entries with
purchaseson the day this shipped. If you reached them before, you did it with a cast. You no longer need to:const sched = await tp.entity(parkId).schedule.upcoming(); for (const day of sched.schedule ?? []) { for (const p of day.purchases ?? []) { console.log(day.date, p.name, p.price.amount, p.price.currency); // 2026-09-08 Lightning Lane for Seven Dwarfs Mine Train 1100 USD } }
Purchases are not limited to
TICKETED_EVENTdays — Lightning Lane entries attach to ordinaryOPERATINGdays, so do not filter ontypeto find them. -
purchases[].price.amountis nullable, matchingPriceData. 7.1.0 madePriceData.amountnullable but the schedule path carried a second, inline copy of the price shape that keptamountnon-nullable. Both now resolve to onePriceData. Tokyo Disneyland serves six Premier Access rows with a null amount right now, so this was a type that disagreed with production. -
Schedule entries gained the
descriptionfield the API has always sent.
-
BREAKING —
tags[].valueis nowunknown. The spec declares no type for it, only a prose description, so the previousstring | number | Record<string, never>was an invention. Narrow before use:const v = entity.tags?.[0]?.value; if (typeof v === 'string') { /* ... */ }
-
BREAKING — nullability tightened where the API never sends null.
locationon entities and children is no longer| null, andpurchases[].typeis no longer| null. Verified against production: 412 sampled children all carried a location, 255 sampled purchases all carried a type. Comparisons againstnullon these will now fail to compile. -
destinationson the destinations response is required rather than optional, and a destination'sparksare typed as their own shape rather than recursively as a schedule response. -
BREAKING — minimum supported Node is now 20. Node 18 reached end of life on 2025-04-30 and is no longer tested.
enginesmoves from>=18to>=20, and CI runs Node 20, 22 and 24.Nothing in the shipped bundle needed Node 18 specifically; the constraint arrives from the dev toolchain, where eslint 10 and vitest 4 both require Node 20 or newer. Rather than keep claiming support for a runtime nothing verifies, the claim is withdrawn. If you are still on Node 18, stay on 7.1.x.
-
Dev dependencies: eslint 9 to 10, vitest 1 to 4.
TypeScript stays on 5.x.
openapi-typescript@7.13.0still declarespeer typescript@"^5.x", so TypeScript 6 cannot be installed here until that range widens upstream.
-
PriceData.amountis nownumber | null, matching the API spec, which has declared this field nullable for some time. The API returnsnullwhen a paid queue exists but the provider does not publish a price;0is reserved for a queue that is genuinely free. The two were previously conflated as0.This is a compile break for strict TypeScript consumers. If you read
price.amountdirectly you will now getTS18047: 'amount' is possibly 'null'orTS2322. Narrow it first:const amount = queue.PAID_RETURN_TIME?.price.amount; const label = amount === null ? 'price not published' : formatCents(amount);
Runtime output is unchanged — the emitted JS is byte-identical, only the type declarations move. Plain-JavaScript and non-strict consumers are unaffected. See MIGRATION.md.
First stable v7 release. After two alpha iterations (alpha.0/alpha.1 blocked
by CI release-pipeline issues, alpha.2 published to next dist-tag) the
public surface is unchanged. Also landed post-alpha.2:
- Docs site deploys the hand-written cookbook alongside the generated API ref.
- README and cookbook examples are plain JavaScript (previously mixed TypeScript syntax into blocks labeled runnable).
- Dependabot action bumps merged (
actions/checkout,deploy-pages,upload-pages-artifact,create-pull-request,action-gh-release).
- Full TypeScript rewrite; dual ESM + CJS output.
- Sync-by-default API built on platform
fetch(Node 18+, browsers, Deno, Bun, Workers). - Ergonomic
tp.entity(id)navigation withwalk(),schedule.range(), discriminated-unionnarrowQueues()andcurrentWaitTime()helpers. - Default-on per-endpoint caching with pluggable adapter.
- 429
Retry-Afterhandling. - Types generated from the upstream OpenAPI spec; post-gen patches not needed (openapi-typescript handles nullability correctly).
- Legacy
Themeparks.DestinationsApi/EntitiesApigenerated surface. See MIGRATION.md. - Babel 7 toolchain,
superagent,mocha.