From aa862d2a17de198f1cf5c0b5b32f8a80afb6b522 Mon Sep 17 00:00:00 2001 From: upstream-mirror Date: Wed, 23 Sep 2026 16:16:53 +0500 Subject: [PATCH] top-up: 8 source files 128-600 KB --- apps/dotcom/browser-run-thumbnails.md | 774 + .../sync-worker/src/TLFileDurableObject.ts | 3531 +++++ .../ni5k8zc.yaml | 4883 +++++++ packages/editor/api-report.api.md | 5520 ++++++++ packages/editor/src/lib/editor/Editor.ts | 11735 ++++++++++++++++ packages/tldraw/api-report.api.md | 6747 +++++++++ .../image-pipeline/worker-configuration.d.ts | 8572 +++++++++++ .../sync-cloudflare/worker-configuration.d.ts | 8202 +++++++++++ 8 files changed, 49964 insertions(+) create mode 100644 apps/dotcom/browser-run-thumbnails.md create mode 100644 apps/dotcom/sync-worker/src/TLFileDurableObject.ts create mode 100644 internal/grafana/resources/dashboards.v2.dashboard.grafana.app/ni5k8zc.yaml create mode 100644 packages/editor/api-report.api.md create mode 100644 packages/editor/src/lib/editor/Editor.ts create mode 100644 packages/tldraw/api-report.api.md create mode 100644 templates/image-pipeline/worker-configuration.d.ts create mode 100644 templates/sync-cloudflare/worker-configuration.d.ts diff --git a/apps/dotcom/browser-run-thumbnails.md b/apps/dotcom/browser-run-thumbnails.md new file mode 100644 index 0000000..845cf36 --- /dev/null +++ b/apps/dotcom/browser-run-thumbnails.md @@ -0,0 +1,774 @@ +# Browser Run thumbnails and MCP screenshots + +Issues: + +- +- + +tldraw.com can capture PNG thumbnails of public boards by taking a Cloudflare Browser Rendering `/screenshot` of a tldraw-owned render page, called straight through the `BROWSER` binding's `quickAction` Quick Actions method — no `@cloudflare/puppeteer` and no API token (requires `compatibility_date >= 2026-03-24`). There are two consumers, both served by the sync worker: + +- an MCP server at `POST /api/app/mcp` exposing `search_boards`, which finds a caller's boards by name, and a drill-down over four more: `get_board_info` lists a board's pages (id, name, 0-based index, and whether each has content), `get_page_info` lists one page's clusters of shapes, `get_cluster_info` describes the shapes inside one cluster, and `get_cluster_screenshot` returns a content-fit PNG of one or more clusters. **It requires authentication** — see "Authentication on the MCP server"; and +- a board OG image endpoint, `GET /api/app/social-preview/:prefix/:slug/image`, built for high-traffic paths (link unfurls, crawlers): it serves only from the R2 cache and delegates rendering to a queue consumer, so a request never waits on Browser Run. It lives under the `social-preview` route family alongside the crawler HTML that references it. + +Rendering runs through the Browser Rendering `/screenshot` Quick Action, invoked from the worker via the `BROWSER` binding's `quickAction` method (`env.BROWSER.quickAction('screenshot', …)`). Chrome runs in Cloudflare's Browser Rendering fleet, not in the worker isolate. The pipeline never hands the browser a user-provided URL: the worker resolves the board, mints a short-lived signed render job, and the screenshot only ever targets the internal render page with that token. **Rendering is not gated on public viewability — serving is.** A thumbnail is generated for every board, private ones included, so that an owner-facing surface has one to show; the OG image route re-applies the public gate on every request. See "Rendering every board" below for what that means for the render token. The MCP surface exposes page metadata, shape clusters — including their shape records, with rich text flattened to a plain string — and cluster screenshots: no arbitrary selectors or arbitrary URLs, and no board the authenticated caller could not already open in a browser. + +## Architecture + +0. The request carries an OAuth bearer token, which the worker verifies before it reads the JSON-RPC body at all. Every call needs one; there is no anonymous tier. See "Authentication on the MCP server". +1. A client calls `get_cluster_screenshot` with a board id — the `:slug` of a file (`https://www.tldraw.com/f/:slug`) or of a published board (`https://www.tldraw.com/p/:slug`) — a page (0-based ordinal or stable page id, default the first page), one or more cluster ids, and an optional theme (default light). It typically calls `get_board_info` and `get_page_info` first to discover pages and clusters; those, and `get_cluster_info`, resolve the board the same way. +2. The sync worker resolves the id **against the authenticated caller**, as a file first and as a published-board slug second, so callers never need to know which kind of board they hold. Files resolve the id directly as the `file.id` and are admitted when `hasReadAccessToFile` says so: the caller owns the file, reaches it through the group that owns it, or the file is shared via link. `sharedLinkType` (`view` vs `edit`) is irrelevant to viewing; deleted files are refused, and test-slug files are refused downstream because they require admin auth. Published boards resolve through the `file` row (`getPublishedFileInfo`) and must be published — the published slug is the whole capability there, so no user check narrows or widens it. Boards the caller cannot see fail without spending any Browser Rendering capacity, and **fail identically to boards that do not exist**, so the fallback is not an existence oracle for file ids. A file admitted this way resolves under `access: 'render'`, which is what lets the tools reach an unshared board; published boards stay `access: 'public'`. See "Rendering every board". +3. The worker resolves the requested page from the board snapshot and clusters its shapes. Clustering needs real geometry and text, which only an editor can produce, so the first call for a given page and content version measures it through a render (`measurePageShapes`) and stores the result as a **cluster index** on the file's durable object; every later call for the same content reads that instead of rendering. See "The cluster index" below. It then resolves the cluster ids against those clusters, and builds an R2 cache key from board identity, a content version, the fixed 1200x630 output size, theme, and a digest of the sorted shape ids (`mcp/{kind}/{slug}/{version}/1200x630/{theme}/shapes-{digest}.png`), with the page name in object metadata. The version is the file's `lastPublished` for published boards and the persisted room snapshot's R2 etag for shared files, so republishing or editing rotates every key. A cache hit in the `MCP_DATA_BUCKET` bucket returns without capturing again, and with the cluster index in place it returns without rendering at all — the key still cannot be built before the cluster ids are resolved, but resolving them no longer costs a render. + + **The access check in step 2 gates this read, not just the render.** These keys carry no viewer dimension, so a private board cached for its owner is a single object that anyone naming the right board id would otherwise be handed. Gating the read rather than adding a viewer to the key is also the cheaper of the two fixes: a viewer dimension would multiply the object count by distinct viewers and turn one shared render into one render per caller. + +4. On a miss, the worker mints an HMAC-signed render token (`renderTokens.ts`) carrying the board identity, the resolved `pageId`, the requested shape ids, and render parameters, expiring in `THUMBNAIL_RENDER_TOKEN_TTL_MS` (60s). The measure render in step 3 mints one the same way with `mode: 'measure'`; both carry the access gate the board resolved under, and `render`-access tokens are recorded as minted — see "Rendering every board". Page enumeration is capped at `MAX_THUMBNAIL_PAGES` (40), so `pageCount` and the addressable ordinals stop there on very large boards. The snapshot route re-checks that `pageId` still exists at render time: a page deleted inside the token's window fails the render rather than returning a different page's image under the original page's name. +5. The worker calls the Browser Rendering `/screenshot` Quick Action through `env.BROWSER.quickAction`, targeting `{MCP_SCREENSHOT_RENDER_ORIGIN}/__thumbnail-render?token=...`. The render page (`apps/dotcom/client/src/pages/thumbnail-render.tsx`) exchanges the token for snapshot data at `GET /api/app/thumbnail-render/snapshot`, which verifies the signature and expiry before returning records, schema, and render params. Published boards read a frozen R2 snapshot; shared files read the live persisted room snapshot from R2 (`env.ROOMS`) and re-check their gate here, not just when the token was minted, so a board deleted during the token's window stops resolving. The page selects the requested `pageId`, fits the camera to the signed shape ids (or the whole page's content when none are signed) with margins once fonts and image assets have settled, exports with `editor.toImage`, and then displays that PNG as a full-viewport `` and sets `data-thumbnail-ready` — so the screenshot captures the exact export rather than the live editor canvas. Any failure (bad token, snapshot load, export, image decode) sets `data-thumbnail-error` instead. The Quick Action waits for _either_ terminal marker and captures `body[data-thumbnail-ready="true"]`, which only exists on the success path — so a failed render returns as soon as it errors rather than holding Browser Run capacity for the whole timeout. The render and settle budgets (`THUMBNAIL_RENDER_TIMEOUT_MS` 45s, `THUMBNAIL_SETTLE_TIMEOUT_MS` 10s) live in `@tldraw/dotcom-shared` so the worker's deadline and the page's can't drift. +6. The screenshot response body is the PNG bytes. The worker writes them to the shape set's cache key in R2 (for future hits) and returns two MCP content items: a text item with the page name, followed by the image. + +### OG images (queue-backed async rendering) + +`GET /api/app/social-preview/:prefix/:slug/image` (`:prefix` is `p` for published boards or `f` for shared files) serves a 1200x630 light-theme, content-fit PNG for use in `og:image` tags. The crawler HTML that references it is the existing worker route `/app/social-preview/:prefix/:slug` (`getSocialPreview`, which Vercel routes crawler user-agents to), which puts the board name in the title and bounces human visitors back to the board. It only emits the board `og:image` (and `summary_large_image`) when the board resolves through the same gate the image route applies; for private, deleted, or unpublished boards it names the static site-wide preview image directly, since pointing at a board image route that has nothing to serve would only cost the crawler a redirect to reach the same file. The request path never invokes Browser Run: + +The route is a **pure read** — two questions and nothing else: + +1. **Is the board publicly viewable?** Resolved through the same gate as the MCP tool: published, or shared via link. Private, deleted, unpublished, or unknown boards get the default tldraw OG image (see the fallback below). This is checked on every request, which is what keeps an unshared board's image unreachable even though the image itself is never deleted. +2. **Does a thumbnail exist?** If so it is served, whatever its state, with `max-age=300` and the object's etag. That is the only thing the version affects — a mismatch is logged as `stale` and served anyway, because an old picture of the board beats the generic tldraw logo, and there is no "too stale to serve". Otherwise, the default-image fallback with `max-age=60`. + +**The lifetime is chosen from the share gate, not from cost or freshness.** R2 reads are a rounding error next to rendering, and unfurl platforms cache a card their own side for days whatever we send, so neither pushes the number around. What the lifetime does decide is how long an image can be served without the gate in step 1 being consulted — and since nothing deletes a board's image when it stops being public, that gate is the only thing keeping an unshared board's thumbnail off the internet. So it is minutes rather than the hour it used to be, `stale-while-revalidate` is absent (it would extend serving a day past expiry, the same objection the default-image redirect already answers), and an `etag` with `if-none-match` support makes the resulting revalidations cheap: a cache that still holds the current bytes gets a 304, and every one of those re-runs the gate. + +The route is registered with `.all`, because crawlers probe with HEAD before (or instead of) GET: a HEAD gets the same cache headers from an R2 `head` but never reads the body. + +**It has no rate limiting, and it enqueues a render in exactly one case.** It used to do both generally. The general enqueue was pointless in the way that matters: unfurl platforms resolve a URL's card once and reuse it for every repost, so the crawler that triggered the render has already cached the default by the time the render lands — a viral link can be fetched once and shown thousands of times, and none of those views come back here. The render was real work whose result nobody fetched. Making the image exist _before_ the board is ever shared is the job of the publish and edit triggers below, not of the request that discovers it missing. With that gone, the per-board limit and the one-hour minimum refresh age that bounded it went too, along with `getOgImageAge`. + +What remains is a **repair for published boards with no image at all** (`repairMissingPublishedImage`), and it exists because the two kinds are not symmetric in how many triggers they have. A shared file re-asks on every persist that advances its document clock, so an ask lost to a queue failure or to a pending marker left behind by an earlier one is made good by the next edit. A published board has exactly one trigger — the publish effect — and its snapshot is frozen, so nothing ever edits it into asking again: one lost ask leaves that board's card generic until somebody republishes. The repair fires only on a total miss (never on a stale image), only for `published`, and on HEAD as well as GET since some crawlers only ever probe. It does not contradict the paragraph above — it is not trying to serve the request that triggers it, it is being the thing that asks when nothing else will. + +Because it is the one render ask an unauthenticated request can cause, it is bounded twice. The pending marker dedupes it while a job is alive, and a **repair cooldown** (`OG_REPAIR_COOLDOWN_MS`, 1 hour) bounds it after one dies: when a crawler-triggered job burns its whole retry budget, the give-up arms a per-board cooldown that the repair consults before enqueueing. Without it, the give-up clearing the pending marker would let the next crawl re-arm a full retry chain immediately — a published board that deterministically fails to render (a huge board that times out, say) would cost a chain of captures every ~4 minutes for as long as anything an attacker controls fetched its URL. With it, such a board costs one chain per hour. Only the crawler-reason give-up arms the cooldown, and only the repair consults it, so a publish-triggered render that fails transiently still gets one immediate repair on the next crawl, and a genuine republish renders straight away regardless. The publish trigger also _clears_ the cooldown, because an in-place republish reuses the slug: the cooldown is evidence about the snapshot that failed to render, and it must not outlive that snapshot and suppress the new one's repair. A republished board that still cannot render simply re-arms it on the next crawler give-up. + +3. The queue consumer (`ogImageQueue.ts`, dispatched from the worker's `queue()` handler) re-resolves the board at render time under `access: 'render'`: a board deleted or unpublished while queued is dropped without rendering, an unshared one still renders (see "Rendering every board"), and the version is re-read so bursts of enqueues coalesce into one capture of the newest content. It loads the snapshot to pick the first page that _has content_ (so a board with an empty first page still unfurls with a meaningful image), mints a render token with `camera: 'content'` and that `pageId`, screenshots it through the same `env.BROWSER.quickAction` path as the MCP tool, and writes the PNG to the cache key the route reads. If the snapshot can't be read it fails there and then rather than paying for a capture that would fail on the render page for the same reason. Genuine transient failures retry up to three times with backoff, then drop. There is no capacity check: thumbnail rendering is uncapped (see "Request limits"). + +#### Default-image fallback + +A board with no usable cached image is sent to the site-wide default (`/social-og.png`, 1200x630, the size the `og:image:width`/`height` meta advertises) with a **302**. The worker does not proxy those bytes: the default is a static asset on the client origin and already cached at the edge, so serving it here would put worker egress in front of every unfurl of an unrendered board. The redirect carries `cache-control: public, max-age=60` with no `s-maxage` and no `stale-while-revalidate`, so nothing pins it under a board's permanent image URL once the real render arrives. Telemetry records it as `not_rendered_yet`. + +**The cost is a generic card, not a broken one.** The crawler follows the redirect and gets a valid image — the tldraw logo rather than the board — and then caches that card for days. So what matters is not how the empty case renders but how often it is reached, which is what the publish and edit triggers are for. What remains exposed is a board shared within the debounce window of its first edit, or one dormant since before this shipped. + +### Keeping the thumbnail current + +Nothing renders on crawler demand any more, so a board's thumbnail has to exist before its first share. Two triggers make that happen, both enqueueing onto the same queue and consumer, and both subject to the same re-resolve and version check at render time. Every queue message carries a `reason` (`publish`, `edit`) that rides through to telemetry; `crawler` survives in the union only as the fallback for messages enqueued before the field existed. + +- **Publish.** `publishSnapshot` (the outbox publish effect in `utils/publishSnapshots.ts`, run by `TLFileEffectProcessor`) enqueues a render right after it writes the frozen R2 snapshot, so a published board's image is being made before its link is pasted anywhere. `unpublishSnapshot` deletes the cached image and pending marker instead — the one outbox effect that touches a thumbnail. **Unsharing has no effect of its own**: every board renders whether shared or not, and the share gate is applied at serve time, so there is nothing derived from a board's public state to tear down when it goes private. +- **On edit.** `TLFileDurableObject.persistToDatabase` schedules a render on a persist that actually advanced the document clock. The only states that skip are `legacy` and `deleted` (see "Rendering every board"); shared and private boards both render. There is no sampling and no staleness window — a persist means the board's saved content genuinely differs from what the cached thumbnail shows, which is exactly when a re-render is warranted. + + **The ask is debounced, not throttled.** Each persist pushes the render deadline out by `OG_RENDER_DEBOUNCE_MS` (60s), so a board renders once its editing _settles_ rather than on a cadence while it is still being drawn on — which is what a thumbnail is for. `OG_RENDER_MAX_WAIT_MS` (5 minutes), measured from the first persist since the last render, stops a board that is never left alone from never rendering. The arithmetic lives in `utils/ogRenderDebounce.ts` so it is testable without standing up a durable object; the object supplies the clock and the alarm. + + **The durable alarm is the deadline**, not an approximation of it. Every persist re-arms it, so the two can never disagree and an eviction loses only the in-memory copy: the alarm still fires at exactly the time the debouncer chose, and the board renders once, when it should. + + This replaced a cheaper scheme where the deadline moved in memory and the alarm was left where it was, re-arming itself on each fire. That wrote storage about once per debounce window instead of once per persist, but it meant the alarm was only a _lower bound_ on the deadline — so an evicted object woke early, rendered, and then rendered again when editing actually settled. The cost moved rather than grew: an alarm write per persist (7.5/min for a continuously edited board), against far fewer alarm **invocations**, since the alarm no longer fires mid-session purely to push itself further out. Ten minutes of unbroken editing goes from ~20 wake-ups to 2 — the two renders themselves. `ogRenderDebounce.test.ts` pins both halves. + + `pendingSince`, the max-wait anchor, is still in memory only. Losing it to an eviction restarts the five minute window, which can delay a render but never duplicate one. + +- **The debounce is the rate control; the pending marker is not.** `OG_PENDING_MARKER_TTL_MS` (5 minutes) reads like a render interval but isn't one — the consumer deletes the marker as soon as a render lands, so on a healthy board it never lives out its TTL. It is a single-flight that stops a second ask being queued while one is in flight, plus a crash ceiling. The TTL is sized above a job's worst-case retry chain (three captures at the full 45s timeout plus 30s and 60s of backoff, ~3.75 minutes) so the marker cannot lapse while its job is still alive — a lapse would let a second job for the same board overlap the first and clobber its render token record. A test pins that inequality. The 45s per capture is enforced, not assumed: the quick action's navigation and settle timers are per-phase and would sum to roughly twice it, so the worker abandons a capture at the single budget (`abandonAtRenderTimeout` in thumbnailRender.ts). To change how often a board renders, change the debounce. + + **The marker drops asks rather than deferring them,** which matters because nothing upstream retries one. The debouncer resets the moment it fires and neither caller reads the enqueue result, so an ask turned away is simply gone. A capture takes seconds, so an ask landing during one hits this: without help, the board would keep a thumbnail that predates whatever prompted that ask until something happened to ask again. + + What covers it depends on the board kind, because the two kinds differ in whether anything re-asks. A **retry** needs nothing either way — every delivery re-resolves before capturing, so a later attempt picks up the newest content by itself. A **shared file's** dropped ask is deferred by construction, in two halves: a debounced fire's ask is only turned away while a job's marker is alive, which puts its persist a full debounce (60s) before the marker's clear, while the image whose write performs that clear read its snapshot at most a capture (45s) plus the write before it — so the content the ask wanted is already in the image; and a max-wait fire, the one ask the debounce doesn't bound, lands at or past the marker's TTL, because the fire that enqueued the job reset the debouncer's window and the marker's expiry is stamped from that fire's own clock reading — so the enqueue's R2 round trip cannot push the expiry past the window it opened. Tests pin both inequalities. The follow-up render that used to sit on top of this was roughly a fifth of shared-file queue captures in production (measured 2026-08-11 via the `followup` telemetry blob) and bought none of it: on a settled board it merely relocated the render the debounced ask was about to do, and on a still-moving board it rendered a mid-edit state the next debounced render superseded. A **published board** is the kind with no second ask — its snapshot is frozen and nothing edits it into asking again — so a completed render re-resolves afterwards and enqueues a follow-up if the version moved under it (`enqueueFollowUpIfBoardMoved`, `published`-only). A job that gives up permanently clears the marker rather than letting it lapse, so the next ask is acted on immediately. + + The deferral stops at that give-up, though: a job that burnt its retry budget wrote no image, so the asks its marker turned away while it was failing deferred into nothing — a board whose editing stopped during that window keeps its stale thumbnail until the next edit. A residue, not a regression: the follow-up only ever ran after a successful capture, so it never covered the give-up path either. + + Follow-ups deliberately **do not chain**: a board republished without pause would otherwise render continuously — the exact cost the debounce exists to avoid. The ceiling is one extra render per triggered render. + +#### Why a debounce and not a throttle + +This started as a 30s leading-and-trailing throttle. Measurement killed it. Production runs **~555 persists/min across ~359 boards active in a 10 minute window** — a mean gap between a given board's persists of roughly **39 seconds**, which is _longer_ than the 30s window. A throttle whose window is shorter than the gap between events suppresses almost nothing: its leading edge fired on nearly every persist, so render volume tracked persist volume rather than the interval it was nominally set to. + +The debounce is a better fit in shape as well as cost. Cost, for a board edited without pause (verified in `utils/ogRenderDebounce.test.ts`): + +| Mechanism | Renders per 10-minute editing session | Isolated burst | +| -------------------------------- | ------------------------------------- | -------------- | +| None | 76 | 1 | +| 30s throttle | ~20 | 1–2 | +| 1/min rate limit | 10 | 1 | +| **60s debounce + 5min max wait** | **2** | **1** | + +Note the right-hand column: on a bursty board — which the 39s mean gap says is the common case — every mechanism costs the same one render. The debounce is not a saving there; it is a saving on the heavy tail of sustained editing, and it renders the _finished_ burst rather than its first stroke. + +Sizing what remains: Browser Run allows 60 new browser instances per minute per account. With a debounce, spend scales with **editing sessions**, not persists and not wall-clock windows, so the quantity to forecast is distinct shared boards starting an editing session per minute. Both inputs — the link-shared fraction `f` and the session shape — now ride on `persist_success` (`blob3` and `index1`), so they are Analytics Engine questions rather than database ones. See "Open questions", and "What it costs" for what the answer is worth in dollars. + +### Request limits + +Thumbnail rendering has **no global cap**, deliberately. It is our own derived artifact, triggered by things that already happened (a publish, an edit persisting) rather than by anything a caller can drive, so a global rate limiter there would only ever mean "serve a stale thumbnail to save a render we intend to do anyway". The queue consumer performs no capacity check at all. + +What bounds it instead is per-board: the render debounce in front of the edit trigger (`OG_RENDER_DEBOUNCE_MS`, with `OG_RENDER_MAX_WAIT_MS` as its ceiling). That is a freshness rule rather than a budget — it does not know or care what the rest of the system is spending — so total thumbnail spend scales with the number of boards being edited at once. See "Keeping the thumbnail current" for how it is sized against Browser Run's account limits, which are now the real ceiling. + +Worth being explicit that **no per-board mechanism can cap total spend**, because total is `active boards × per-board rate`. Cloudflare's rate limit binding cannot close that gap either: limits are applied per Cloudflare location, so a single global key gives `limit × locations`, not `limit`. (The same caveat applies to the MCP global cap below — `MCP_GLOBAL_BROWSER_RUN_RATE_LIMIT = 20` is really 20/min per colo. It bounds a rogue caller, which is its job, but it is not an account-wide spend ceiling.) The levers that actually bound the total are Browser Run's own account limits and raising them. + +The limits below are therefore the **only** rate limiting in the pipeline, and they exist for the **MCP endpoint** specifically — the one Browser Run-spending surface an outside caller can drive directly, where a rogue or looping agent is the threat being bounded. They are applied in `mcpServer.ts` (with the fallback budgets in `config.ts`), not in the shared render core, so a new surface built on those helpers cannot pick one up by accident: + +- Per account: ~10 Browser Run-spending **calls** per minute, one budget across every such tool (`user:` on `MCP_SCREENSHOT_RATE_LIMITER`). +- Per board: ~2 Browser Run **captures** per minute (`board:` on `MCP_SERVER_BOARD_RATE_LIMITER`), applied only on cache misses. +- Global: ~20 Browser Run **sessions** per minute across all MCP callers (`MCP_SERVER_BROWSER_RATE_LIMITER`, key `global`), captures and measures alike. + +The per-account budget is one bucket, and counts calls rather than captures. Both matter. It was three buckets — one per tool family — which made the real per-caller ceiling three times the single number quoted here and in every refusal message. Counting calls is what keeps it meaningful now that most calls spend nothing: with the cluster index in place the ordinary one-screenshot flow (`get_page_info`, `get_cluster_info`, `get_cluster_screenshot`) is still three calls but only two browser sessions — one measure and one capture — where it used to be four. ~10 calls a minute is therefore nearer three finished screenshots a minute in Browser Run terms and rather more than that in tool calls. Worth revisiting on the binding before the flag is widened. + +Measures are counted against the **global** budget but deliberately not the per-board one. They are real Browser Run sessions and used to be counted against neither, which is what made the account-wide ceiling several times the one actually enforced. Per-board allows 2 a minute, and a cold drill-down can legitimately measure once and capture once against the same board, so counting measures there would refuse the documented path rather than an abusive one. The global check now runs only when the cluster index misses: a call served from the index creates no session, and metering it would be metering nothing. + +Per-board is deliberately far tighter than per-account: no single board may absorb more than 2 captures a minute. Because captures are counted only on cache misses, this does not bound the usual "screenshot several clusters of one board" flow — a repeated capture of the same shapes is a cache hit. + +That gap is only expressible because each budget has **its own binding**. A binding carries a single `limit` applied per key, so two budgets wanting different numbers cannot share one however distinct their keys are. The caller and per-board budgets shared `MCP_SCREENSHOT_RATE_LIMITER` while both were 2, which made them look separable when they were not; per-board moved to `MCP_SERVER_BOARD_RATE_LIMITER` (`namespace_id` 1013–1016) when they diverged. + +The `MCP_SERVER_` prefix on two of the three is deliberate. Neither budget is about screenshots as such — they bound Browser Run spending by the MCP endpoint, and the cluster tools land on the same allowance, so a name tied to one tool is wrong the moment a second tool spends it. + +**The caller budget keyed on `cf-connecting-ip` until the endpoint required authentication.** An account is the better key in both directions: a proxy pool no longer buys a caller more budget, and everyone behind one NAT no longer shares a single one. The binding keeps the name `MCP_SCREENSHOT_RATE_LIMITER` because it is deployed and counting; only the key inside it changed, which resets the buckets once and then behaves. IP-keyed limits still make sense on endpoints with no caller identity — the render page's snapshot route, and the unauthenticated discovery endpoint — and there are none there today. + +Two things to know when changing any of these. The numbers live in **two places that must move together** — the constants in `config.ts` (`MCP_PER_USER_RATE_LIMIT` and friends) are only the isolate-local fallback for local dev and tests, and every deployed environment is governed by the Cloudflare binding in `wrangler.toml`, so editing one alone changes nothing where it matters. Unit tests run with no bindings at all, so they pin the fallback constants and can never catch a wrong or shared binding; `wrangler.toml` is the only place to check that. And `period` in those bindings [must be either 10 or 60 seconds](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) — that restriction is on the window, not on `limit`, which is an unconstrained integer. All of these use `period = 60`. + +Only the calls that _can_ spend Browser Run are limited. `get_board_info` is not: it resolves a board and reads its snapshot, which is the same work the ordinary board routes already do for that caller, and it never clusters. `search_boards` is not either, though for a different reason — it spends no Browser Run, but its Postgres cost is not comparably cheap; see "MCP tools" below for what it actually does and why a dedicated limiter is deferred rather than added. The clustering tools are limited even though they read as "info" calls, and even though the cluster index means most of them spend nothing — the per-account budget is a ceiling on calls, and whether any given one turns into a render is not something the caller controls. The OG image route's per-board limit (`og-board:`) is gone too, along with the route's enqueue — see "OG images (queue-backed async rendering)" above. + +The Cloudflare rate limit bindings are declared in `wrangler.toml` for every environment. When a binding is absent (local dev, tests) the route falls back to an isolate-local guard with the same limits. Changing the global cap means moving the `MCP_SERVER_BROWSER_RATE_LIMITER` bindings in `wrangler.toml` (one per environment) and the isolate-local fallback constant `MCP_GLOBAL_BROWSER_RUN_RATE_LIMIT` in `config.ts` together. + +The thing to watch instead of a cap is Browser Rendering's own account limits: a render can hold a session for the full 45s `THUMBNAIL_RENDER_TIMEOUT_MS`, so sustained edit volume is bounded in practice by concurrent-session and new-session-per-minute limits rather than by anything in this worker. Browser Rendering bills by browser duration, so thumbnail spend now scales with editing activity. `fetch-screenshot-metrics.ts` (below) is how that gets watched. + +### What it costs + +Quick Actions bill [**duration only**](https://developers.cloudflare.com/browser-run/pricing/) — $0.09 per browser hour beyond the 10 hours a month Workers Paid includes. The per-concurrent-browser charge applies to Browser Sessions, which this pipeline does not use, so concurrency is a limit here rather than a line item. A capture that fails on a `waitForTimeout` is not charged at all, which means telemetry's wall-clock `double3` is an upper bound on the bill rather than the bill. + +**What it costs today, measured.** The Browser Run dashboard (Compute > Browser Run) for July 2026: **15.22 browser hours across 8.75k Quick Action requests**, all of them `Screenshot` against `www.tldraw.com/__thumbnail-render`, no Browser Sessions at all. Volume was near zero until about July 20 and has run at **~1,000 captures and 1.6–2.6 browser hours a day** since — call it **0.7 renders/min**, against a **$0.47** bill for the month and a **$4–6/month** run rate if that holds. This is the pre-branch pipeline: the edit trigger is not deployed, so what is being measured is publish- and crawler-driven rendering only. + +**Those hours are all thumbnails.** Browser Run reports one account-wide figure that both surfaces land in, but the buckets separate them: `dotcom-mcp-data-preview` holds **0 objects**, and the production `dotcom-mcp-data` bucket does not exist yet. Since every MCP capture writes exactly one object and its key carries the board's content version, an empty bucket is an unused surface — so the whole 8.75k is the OG pipeline, and the MCP tool's share of the bill is currently zero. That also makes the object count the standing way to split the two later: MCP captures over the last 30 days are just the object count in that bucket, since the lifecycle rule expires them on exactly that schedule. + +The useful number that falls out of it is the **mean capture: 6.3 seconds** (15.22h / 8.75k). That is what the forecast below is priced at, rather than an assumption, and it is worth re-deriving the same way whenever these figures move — two dashboard numbers divided by each other. + +At that 6.3 seconds, one render is **~$0.00016 of browser time** and **~$0.000018 of everything else**: three R2 class A writes (the pending marker, the render token record, the PNG — deletes are free), roughly seven class B reads across resolve, cache check, snapshot load and token check, three queue operations, and a couple of worker requests. So ~$160 per million renders against ~$18 for the machinery around them, and the only quantity worth forecasting is the render count. + +**Renders scale with editing sessions, and the session shape is now measured.** [PR 9708](https://github.com/tldraw/tldraw/pull/9708) put `index1` (the durable object id) on every file DO event in production, which is exactly what open question 2 needs: `persist_success` rows carry a per-board key, so a board's persists can be bucketed by time and the runs between them counted. Replaying the debounce's own rules over a 30 minute production window (`blob1 = 'persist_success'`, `blob2 = 'production-tldraw-multiplayer'`, grouping by `index1`, splitting a board's persists wherever the gap exceeds `OG_RENDER_DEBOUNCE_MS`, and adding one render per `OG_RENDER_MAX_WAIT_MS` a run survives): + +| Quantity | Measured | +| ------------------------------------------- | ----------------------------------------- | +| Boards persisting in the window | 553 | +| Persists | 521/min (independently confirms the ~555) | +| Editing time | 69.5 board-minutes per minute | +| **Renders — sessions settling** | **34–51/min** | +| **Renders — `OG_RENDER_MAX_WAIT_MS` fires** | **0.3–4/min** | +| **Total** | **~37–51/min, ~54,000–74,000/day** | +| Browser hours/month | 2,800–3,900 | +| **Browser Run $/month** | **$250–350** | + +The range is sampling, not uncertainty about the mechanism: Analytics Engine thins high-volume indexes, and a thinned timeline splits runs that were really continuous, so the raw count is an upper bound. The low end divides each board's gaps by its own `_sample_interval` before applying the 30 second rule. Both ends assume nothing — they are the branch's constants applied to production timings. **Call it $250–350/month and plan against ~$300.** + +Two things fall out of the measurement that the model got wrong: + +- **Renders per persist is 0.07–0.10.** The debounce suppresses about 90% of persists, which is the claim in "Why a debounce and not a throttle", now with a number on it. +- **Editing is far more fragmented than assumed.** The median session spans **8–12 seconds** and the p90 spans 86–216 seconds; in a 10 minute window, 105 of 251 persisting boards persisted exactly once. Sessions are short bursts, not the multi-minute stretches the estimate assumed, which is why the render count is close to one per board-editing-visit. + +One caveat on the figure: `persist_success` on `main` carries no `sharedState`, so these counts include `legacy` and `deleted` rooms that the trigger skips. The measurement is an over-count by that fraction, and this branch's `blob3` is what nets it out. + +#### The two constants only work as a pair + +The same replay run over six hours (3,507 boards) against a grid of both constants, in renders/min and Browser Run $/month: + +| Debounce ↓ / max wait → | 5 min | 10 min | 15 min | off | +| ----------------------- | --------------- | ----------- | ----------- | ----------- | +| 30s | 35.7 · $240 | 32.1 · $216 | 31.3 · $211 | 30.5 · $205 | +| **60s (current)** | **32.3 · $218** | 26.7 · $180 | 25.1 · $169 | 23.3 · $157 | +| 120s | 32.0 · $215 | 24.4 · $164 | 22.0 · $148 | 19.0 · $128 | + +Read along the first column: at a 5 minute max wait, quadrupling the debounce from 30s to 120s saves almost nothing (35.7 → 32.0). The reason is mechanical — merging two bursts into one longer session pushes that session past `OG_RENDER_MAX_WAIT_MS`, so a settle render is traded for a max-wait render, and `OG_RENDER_MAX_WAIT_MS` puts a floor under what the debounce alone can achieve. The savings live on the diagonal: 60s with a 10 minute max wait is ~25% fewer renders, 120s with 15 minutes is ~38% fewer. + +The debounce is at 60s and the max wait stays at 5 minutes, which buys roughly 10%. That is a deliberate choice rather than the cheapest point on the grid: a board edited without pause keeps a five minute old thumbnail rather than a ten minute old one, and the cheaper cells are available whenever spend matters more than that. + +**Raising the account limit is still worth doing, and the grid is why.** Browser Run allows 60 new browsers/min. The mean is ~32 renders/min at the current constants, but the limit applies per minute and the distribution has a tail: p99 is ~47/min and the busiest minute observed reached 71–75/min — most likely a wave of rooms persisting together after a deploy or an eviction, which is exactly the shape that breaches a per-minute limit. `enqueueFollowUpIfBoardMoved` no longer adds to it: follow-ups are `published`-only, and publish renders are a rounding error next to the edit volume. So the debounce change moves the mean but barely touches the peak, and the peak is what the limit sees. Past the limit the spend stops growing and the failure rate starts — a rejected render retries three times holding its pending marker, so overload amplifies rather than backs off. **Ask for the increase before the deploy**, and watch renders/min against 60 from the first hour. + +**Render page egress is plausibly larger than the browser time, and is not Cloudflare's.** Every capture is a cold browser loading `/__thumbnail-render` from the client origin: the lazy route chunk, the editor bundle, fonts, and whatever image assets the board contains. At a nominal 2 MB transferred that is ~2 TB and a few hundred dollars per million renders — so at the measured ~60,000 renders/day it is ~3.6 TB and **~$540 a month, more than the browser time itself**, and on a different bill. The transferred size of one render is worth measuring before trusting the magnitude, and it is the one cost that can be cut without touching thumbnail freshness. + +Two smaller lines, both fixed rather than per-render: + +- **~$24/month of durable object alarm writes.** `scheduleOgRender` calls `setAlarm` on every persist and [each `setAlarm` is one row written](https://developers.cloudflare.com/durable-objects/platform/pricing/#sqlite-storage-backend) — ~24M rows/month at $1.00/M. This is the cost the alarm-is-the-deadline design deliberately took on, and it is paid whether or not a render results. See "Keeping the thumbnail current" for what it buys. +- **R2 storage is a rounding error.** The `og/…` keys carry no version, so it is one object per board: the production `thumbnails` bucket currently holds **4.71k objects in 404 MB** (~86 KB a thumbnail, plus the zero-byte markers and token records), which is well under a cent a month. Growth is bounded by board count rather than by render volume, so rendering every board on every edit does not move this line. The `mcp/…` keys do accumulate, which is what their expiration rule is for. + +The MCP surface prices separately, since it is driven by callers rather than by editing: at the full `MCP_GLOBAL_BROWSER_RUN_RATE_LIMIT` of 20 captures/min it is ~$135/month, and because Cloudflare rate limits apply per location that is per colo. It bounds a rogue agent, which is its job; it does not bound spend. + +If this needs to come down, in order of leverage: shrink what the render page loads, which is the largest line and costs no freshness at all; then raise `OG_RENDER_MAX_WAIT_MS` alongside the debounce, since the grid above says neither constant does much alone; then cap the render rate outright with the dedicated queue described in "Follow-up work". Prioritising by `sharedState` — a longer debounce for private boards than for shared ones — is the option that goes below the floor of one render per board-editing-visit without capping anything, and the durable object already knows which is which at trigger time. + +### Telemetry and monitoring + +Telemetry is two events in the `MEASURE` dataset, split by concern: the cache is a request-level question and Browser Run spend is a session-level one, and one request can hold zero, one, or two browser sessions (a cluster screenshot on a cache miss measures _and_ captures), so no single row can answer both cleanly. + +**`mcp_shared_board_screenshot` — one row per request or queue delivery.** All three surfaces write it with the same blob layout, so one dashboard covers everything; the source blob distinguishes `mcp` (the tool), `og` (the OG image route), and `queue` (the async consumer). Rows record cache status, failure reason, rate-limit decisions, a hashed caller, and the trigger that asked for the render. Cache status is `hit`/`stale`/`miss` when the PNG cache was consulted and `none` when it was not — every info-tool row (those tools have no cache), and screenshot requests refused before the cache read (bad input, rate-limited caller, unresolvable board). A hit-rate-by-source panel should exclude `cache:none`; that is what keeps it reading cache health rather than refusal volume, and what keeps the MCP number comparable to the OG one. Rows carry **no board identity at all** — no index, no slug, no hash, no derived id (see below). Two dimensions are deliberately kept low-cardinality: the failure reason is always a bounded reason code (`invalid_input`, `not_found`, `board_empty`, `page_not_found`, `cluster_not_found`, `shape_not_found`, `rate_limited_user`/`board`/`global`, `board_not_viewable`, `not_rendered_yet`, `snapshot_read_error`, `board_lookup_error`, `browser_failed`, `browser_timeout`, `empty_render`, `not_configured`, `render_error`), never raw `error.message` text; and the hashed caller is written only on **rate-limited** rows — every other row carries `caller:none`. That is deliberately narrower than "any failure": cardinality is about distinct values rather than row count, and most failures are routine model mistakes (a wrong board id, a stale cluster id, a page that moved) that nearly every caller makes eventually, so gating on failure would let the distinct-caller count converge on the number of users. Rate limits fire rarely and only for callers worth naming, which is the question the blob exists to answer. The caller is a hashed **user id** on the MCP surface, which requires authentication; it was a hashed client IP before that, and holds the same blob position so the dashboard panels reading it did not move. The OG surfaces have no caller and always write `none`. Column layout: `blob1` event name, `blob2` worker name, `blob3` source, `blob4` cache status, `blob5` failure reason, `blob6` rate-limit decision, `blob7` hashed caller (or `none`), `blob8` render trigger (`crawler`, `publish`, `edit`, or `none` on the surfaces that have no trigger), `blob9` follow-up state (`true` for the delivery a completed render enqueues because a `published` board — the only kind that follows up — moved during its capture, `false` for a trigger-requested queue delivery, `none` on the surfaces that have no follow-up concept), `blob10` cluster index status (`hit`/`miss` on the three clustering tools, `none` everywhere else — its own dimension rather than folded into `blob4`, because the two caches save different things and one blob mixing them would make either hit rate unreadable), `double1`/`double2` output width/height, `double3` and `double4` always -1 (durations moved to the session event; the sentinels hold these positions so historical rows — which carried capture durations in `double3` — and the panels reading later positions keep their meaning), `double5` rate-limit allowed (1/0), and no `index1`. + +**`browser_run_session` — one row per browser session actually created**, written at the render choke point rather than by each surface, so a session cannot exist without landing on this ledger. `count()` is the number of sessions; `sum(double3)` is the spend. Column layout: `blob1` event name, `blob2` worker name, `blob3` source (`mcp` or `queue`; the OG route never creates sessions), `blob4` mode (`measure` for the clustering tools' geometry render, `screenshot` for a capture), `blob5` outcome (`ok`, or the session-level failure codes `browser_failed`, `browser_timeout`, `empty_render` — a session that died still spent its duration), `blob6` render trigger (as above), `blob7` page reach (`reached` when the render page fetched its snapshot during the session — every session that produced a capture, and the dead ones whose page ran before stalling; `unreached` for a dead session whose page never called home, which is Browser Run or script delivery rather than the page; `unknown` where there was nothing safe to ask — no bucket, or a `public` MCP capture, whose token outlives its cleanup and so is never stamped), `double1`/`double2` viewport width/height, `double3` the session's wall-clock ms, `double4` the browser ms Browser Run billed (`X-Browser-Ms-Used`; 0 for a session that died before a response or ran on the local dev service — `double3 - double4` is queueing and transfer), `double5`–`double9` what the board held: record count, shapes on the rendered page, image and video assets, and the width and height of the top-level shapes' bounding box from their stored positions and sizes. Those five are how render time and failure get correlated with content without any board identity: numbers carry no cardinality, and they are bucketed at query time. No caller and no board identity, deliberately: abuse analysis lives on the request event, and the no-board-identity rule below applies here too. + +One event outside that dataset matters for sizing: `persist_success` (same `MEASURE` dataset, written by `TLFileDurableObject.logEvent`). It fires on exactly the event that triggers a thumbnail render, so it carries what sizing that render needs: `index1` the **durable object id**, `blob3` the board's `sharedState` (`shared`, `private`, `unknown` for an app file whose record hasn't loaded, `legacy` for a non-app room, `deleted` for a deleted file), and `double1` the retry attempt count. It is written by `getBoardRenderState`, the same method that gates the render, so a board the trigger skips can never be counted as one it renders. + +The index is stamped centrally in `writeEvent` for every file DO event, not per call site. Analytics Engine samples by index, so a board that persists rarely keeps data points instead of being sampled away inside the volume of a busy one, and since it has no `uniq()`/`count(distinct)`, distinct boards means `GROUP BY index1` and counting the returned rows. + +It is the durable object id rather than the board slug on purpose: `idFromName` is one-way, and for an app file the slug _is_ the authority of `tldraw.com/f/`, so writing it to an account-readable dataset exported to Grafana would put working capabilities in telemetry. Resolving in the useful direction still works from a slug you already hold, via `env.TLDR_DOC.idFromName('/r/' + slug)`. + +Only `persist_success` carries that index. The screenshot events deliberately carry none (see "No board identifier leaves this pipeline"), so the two datasets cannot be joined and renders-per-persist is two aggregate counts divided by each other rather than a per-board number. + +The one-way-ness is also why `sharedState` has to be recorded at write time rather than recovered later: the dataset cannot be joined back to a file row. Postgres could not answer it anyway — it knows which files are shared, not which are being edited. See "Open questions". + +Bounded reason codes say _that_ a board stopped rendering, never _why_, and every one of these surfaces deliberately swallows its own errors (the OG route falls back to the default image, the snapshot route 404s, the MCP tools return a tool error, the queue retries or drops). So each swallow point also reports the underlying error to Sentry through `reportThumbnailError` (`thumbnailShared.ts`), tagged `thumbnail_surface` with a closed set of values: `og_route`, `og_queue`, `thumbnail_snapshot`, `mcp_board_info`, `mcp_screenshot`, `mcp_board_search`, `mcp_screenshot_cache_write`, `mcp_cluster_index_read`, `mcp_cluster_index_write`. The last three are never caller-visible failures — the tool answered — but each means a cache stopped absorbing what it exists to absorb, so the tools are back to spending Browser Run on every call. Reporting rides on the handler's `waitUntil` and is itself failure-proof — a missing Sentry env var must never turn a degraded-but-fine response into a 500. + +#### No board identifier leaves this pipeline + +**No board identity is written anywhere** — not to telemetry, not to a log line, not to a Sentry event. Not the slug, not a hash of it, not a derived durable object id. For a link-shared file the slug _is_ the file id, and `tldraw.com/f/` is the capability to view a board somebody chose to share by link rather than publish; a one-way derivation avoids handing that out, but it is still a per-board dimension in an account-readable dataset, so the simplest guarantee is to record none. + +Three routes out, and the third was the sharp one: + +- **Telemetry** writes no `index1`. It briefly carried the board's durable object id so renders could be joined to the `persist_success` events that caused them; that join is gone deliberately, and with it the ability to say which board is failing from the dataset alone. These datapoints answer aggregate spend and failure-rate questions. +- **Sentry extras** carried raw `slug`/`boardId` at six call sites, then briefly a derived id. They now carry neither — only `kind`, `prefix`, `page`, `theme`, `attempts` and the like. +- **The request object** is no longer handed to `createSentry` at all. It passes one straight to Toucan with `allowedSearchParams: /(.*)/`, which records the full URL and every query parameter — and on these routes the URL is the sensitive part. `/app/social-preview/f//image` carries a link-shared file's id in its path, and `/api/app/thumbnail-render/snapshot?token=…` carries a signed render token, which is a live capability to read that board's entire snapshot until it expires. `reportThumbnailError` now takes only the method and user agent from the request; the `thumbnail_surface` tag already says which endpoint it was. + +`TLFileDurableObject`'s render log had the same issue and now logs only the enqueue result — no slug and no derived id. + +#### Reading a Browser Run failure + +`422 Unprocessable Entity` is the status to expect from a failed capture, and on its own it says almost nothing. Cloudflare answers 422 for [every "the page did not cooperate" outcome](https://developers.cloudflare.com/browser-run/faq/): a page that crashed, a render that exhausted the container's memory, and any of the [Quick Action timers](https://developers.cloudflare.com/browser-run/reference/timeouts/) expiring. Our own render page marking `data-thumbnail-error` arrives as one too, because the capture selector (`body[data-thumbnail-ready="true"]`) exists only on the success path — that is the design working, and it is indistinguishable by status from the cases that aren't. + +What separates them is the response body, so `runRenderSession` reads it and throws a `BrowserRenderError` carrying the status, Cloudflare's own message (its `errors[].message`, truncated), the wall-clock duration, and the timeout budget. Two things follow: + +- **`classifyScreenshotFailure` splits 422 into `browser_timeout` and `browser_failed`** from that body, falling back to "did the call spend essentially the whole budget" when the body names no timer. It used to classify on `error.message` alone, which for a Browser Run failure never contains the word "timeout" — so every timeout was filed as `browser_failed` and the dashboard's timeout rate was structurally always zero. +- **The specifics reach Sentry as event context, not as the message.** Sentry groups on the message, so it stays exactly `Browser Rendering screenshot failed ()` and the varying parts ride on `browser_render_status`, `browser_render_detail`, `browser_render_duration_ms`, `browser_render_timeout_ms`, and `browser_render_reason`. Putting the detail in the message would shatter one recurring issue into a stream of new ones. + +The queue consumer reports **once per job, on the delivery that gives up**, rather than once per delivery. A board that fails deterministically fails all `OG_MAX_RENDER_ATTEMPTS` times, so per-delivery reporting filed three events for one problem. A failure that recovers on retry now reports nothing, which is correct — the render landed. + +Telemetry goes the other way on purpose: **one datapoint per delivery, and a failed capture records its duration in `double3` like a successful one.** The split is that telemetry counts spend and Sentry counts problems — three deliveries are three lots of Browser Run and one problem. This used to write nothing at all on the retried deliveries and `-1` for the duration on the final one, so a failing board's spend was invisible on a path that has no cap and is watched only by this dataset. A delivery that bails before the capture (an unreadable or empty snapshot) still records `-1`, because it genuinely spent nothing. + +If timeouts turn out to dominate, the knob Cloudflare points at is `actionTimeout` (default none, max 5 minutes), which bounds the capture itself rather than the page load; it is not currently set in `getThumbnailScreenshotRequestBody`. + +`internal/scripts/fetch-screenshot-metrics.ts` queries the Analytics Engine SQL API and reports request volume, failure rate, timeout rate, cache hit rate, rate-limit blocks, and Browser Run render time per source (wall-clock `double3`, summed over rows that actually rendered — `double4` billed ms is not currently recorded, always -1): + +```bash +CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_ANALYTICS_API_TOKEN=... \ +npx tsx internal/scripts/fetch-screenshot-metrics.ts --last 24h --worker main-tldraw-multiplayer +``` + +For alerting, run it with `--check` on a schedule (cron CI job or any monitor that can run a command); it exits non-zero when a threshold is breached: + +```bash +npx tsx internal/scripts/fetch-screenshot-metrics.ts --last 1h --check \ + --max-failure-rate 0.2 --max-timeout-rate 0.1 --max-render-minutes 60 +``` + +The API token only needs the "Account Analytics: Read" permission. Ad-hoc dashboard queries can use the same SQL API, e.g. failure breakdown over the last day: + +```sql +SELECT blob3 AS source, blob5 AS failure, SUM(_sample_interval) AS requests +FROM MEASURE +WHERE blob1 = 'mcp_shared_board_screenshot' AND timestamp > NOW() - INTERVAL '24' HOUR +GROUP BY source, failure +ORDER BY requests DESC +``` + +## Configuration + +The sync worker needs: + +- `BROWSER` binding - the Cloudflare Browser Rendering binding, declared per environment in `wrangler.toml` (`[env..browser]`). The worker calls its `quickAction` Quick Actions method (`env.BROWSER.quickAction('screenshot', …)`) directly — no `@cloudflare/puppeteer`, no API token. This requires `compatibility_date` `2026-03-24` or later, which the deployed envs use; `[env.dev]` pins an older date the local workerd supports (see Local development). The dev binding is deliberately not `remote`, so plain `wrangler dev` (and the credential-free e2e stack) boots without a `CLOUDFLARE_API_TOKEN`; the binding is then a non-functional local one. Local dev therefore never reaches this binding at all — it renders through `LOCAL_SCREENSHOT_SERVICE_URL` instead (see Local development). +- `LOCAL_SCREENSHOT_SERVICE_URL` - development only. An HTTP screenshot service to use instead of the `BROWSER` binding, set in `[env.dev.vars]` to the client dev server's `/__screenshot` endpoint. Deployed environments leave it unset, which is what keeps them on Browser Run: the renderer is chosen by whether this var is set, not by an environment name, so an environment can only take the local path by configuring one. +- `MCP_SERVER_ENABLED` - kill switch for the MCP server (`POST /app/mcp`), set to `"true"` in `wrangler.toml` for dev, staging, and production. The worker reads it per request, so setting it to anything else takes the endpoint down (it 404s everything, including `server/discover` and the legacy `initialize` handshake) without a rebuild or a code deploy — flip it in the Cloudflare dashboard under the worker's variables, and it applies to the next request. The next deploy overwrites the dashboard value from `wrangler.toml`, so follow an emergency flip with a config change. An unset var counts as enabled, so preview deploys (which don't set it) behave as they always have. Only the MCP server is gated: OG image rendering has its own path and keeps running. +- `MCP_SCREENSHOT_TOKEN_SECRET` (deploy var, GitHub secret) - HMAC secret for render tokens. Local dev uses the placeholder in `[env.dev.vars]`. +- `MCP_SCREENSHOT_RENDER_ORIGIN` - set in `wrangler.toml` for dev (`http://localhost:3000`), staging, and production. Preview deploys have no `wrangler.toml` entry, so `deploy-dotcom.ts` injects the preview's own client origin (`https://${previewId}-preview-deploy.tldraw.com`) as a deploy var. +- `MCP_SERVER_URL` - the MCP server's public URL: the resource identifier advertised in protected resource metadata and pointed at by the `WWW-Authenticate` challenge. **Not** compared against an incoming token — Clerk stamps no `aud`, so there is no audience binding to check; see "Authentication on the MCP server" for what stands in for one. Set in `wrangler.toml` for dev, staging, and production; previews have no entry, so `deploy-dotcom.ts` injects `https://${previewId}-preview-deploy.tldraw.com/api/app/mcp` as a deploy var. Left unset it would be derived from the request's own `Host` header, which is acceptable for local dev and tests only — see `getMcpResourceUrl`. +- `THUMBNAILS` R2 bucket binding - board thumbnails / OG images (`og/…` keys) and their pending markers. `thumbnails-preview` in dev/preview/staging, `thumbnails` in production. +- `MCP_DATA_BUCKET` binding - R2 bucket for MCP tool output; today that is screenshots (`mcp/…` keys). `dotcom-mcp-data-preview` in dev/preview/staging, `dotcom-mcp-data` in production. + +### Why two buckets + +Both key spaces used to live in `thumbnails`, separated only by prefix. They are now separate buckets for two reasons: + +- **Domain.** `MCP_DATA_BUCKET` is where the MCP surface puts what it produces, and that won't stay limited to board thumbnails. Keying the bucket to the tool rather than to the artifact means the next MCP output type lands somewhere that already fits, instead of accreting inside a bucket named for something it isn't. +- **Retention.** The two caches want opposite lifetimes: + - **`og/…`** keys (`og/{kind}/{slug}/{theme}.png`) carry no version, so each render overwrites the same object in place and a board costs exactly one object for as long as it exists. Nothing accumulates and nothing deletes one, and the current thumbnail must outlive any lifecycle window — so `THUMBNAILS` gets **no expiration rule**. + + That is also why the key holds nothing but the board and the theme — in particular **not the output dimensions**. This key is the image's sole address, so anything in the path that can change re-addresses every board's image at once and strands the old objects permanently, since there is no lifecycle rule to sweep them. A size change is a replacement rather than a second object, so it belongs in the object's metadata, which overwrites in place. The trade is that a size change serves old-sized images as fresh hits until each board next renders, because the stored `version` tracks board content, not render parameters. + + - **`mcp/…`** keys include the board's content version (`mcp/{kind}/{slug}/{version}/{w}x{h}/{theme}/page-{n}.png`), so every edit strands the previous object and the set grows without bound. A pure regenerable cache, so `MCP_DATA_BUCKET` gets an **expiration rule**. + +A prefix-scoped lifecycle rule on a single bucket would also work (`wrangler r2 bucket lifecycle add` takes a prefix positionally), and has the nice property of ageing out the existing backlog in place. It was rejected because a future rule added without a prefix, or with a typo'd one, would silently delete every board's live thumbnail, and R2 expiration has no undo. Separate buckets make that mistake impossible. + +#### Nothing deletes a rendered image + +Whether an image is deleted when a board stops being publicly viewable depends on which key it is, and the two are **not symmetric**: + +- **`og/shared_file/{fileId}/…` is kept.** Its key is the file id, which never changes, so it stays useful for as long as the board exists — an owner-facing surface behind auth wants it, and switching the link back on makes it an immediate cache hit rather than a cold render. Unsharing clears only the pending marker. +- **`og/published/{publishedSlug}/…` is deleted on unpublish.** It depicts a published _snapshot_, and unpublishing destroys the thing it was a picture of. Its key is the published slug rather than the file, so leaving it would strand an object that a regenerated publish link could make permanently unreadable — nothing would ever read or overwrite it again. `deleteOgImage` is scoped by the board passed in, so unpublishing cannot touch the same board's file-keyed image. + +The queue consumer applies the same rule when it drops a job for a board that no longer resolves. + +The argument for keeping the file-keyed half: an unshared board's thumbnail does depict content that is no longer public, but the image is not public because it exists — it is public because a route serves it, and the only route that does re-checks the share gate on every request (`resolveThumbnailBoard` in `getOgImage`). An unshared board's image is already unreachable while it sits in R2. + +Keeping it means an owner-facing surface behind authz — a workspace or project view showing every board's thumbnail — can use the image a board already has, instead of it having been thrown away the moment the board went private. The `og/…` keys carry no version, so retaining them costs one object per board and does not accumulate. + +`deleteOgImageCache` is therefore now `clearOgImagePendingMarker`, which drops only the `.pending` marker. That part is still load-bearing: a marker left behind would dedupe away the next legitimate enqueue after a reshare, or after the render that failed. + +**Hard deletion is where both halves go.** `TLFileDurableObject.appFileRecordDidDelete` — the same cleanup that removes the room snapshot, the edit history and the published history — deletes the file-keyed image, the published-slug image, the pending and repair-cooldown markers, and every render token record under both boards' prefixes (listed rather than addressed directly, since the keys are per surface and, for MCP, per page and theme). Neither reason for keeping an image survives here: there is no board left to reshare, and no snapshot left to depict. It matters more than tidiness reads, because these keys carry no version, so each board owns exactly one object in a bucket that has **no lifecycle rule and must never get one** — an image left behind by a deleted board is an object nothing will ever read, overwrite, or sweep. MCP screenshots need no equivalent: their keys carry a content version and their bucket expires them. + +### Rendering every board + +Thumbnails are generated for **every** board, not only publicly viewable ones, so an owner-facing surface (a workspace or project view, behind auth) always has a current image to show. Sharing is a condition of _serving_, not of _rendering_. + +That is expressed as an explicit access level, `ThumbnailBoardAccess`, required at every call site of `resolveThumbnailBoard` and `loadBoardSnapshot` rather than defaulted — a default would be wrong for half of them, and silence is the wrong way to choose a gate: + +- **`access: 'public'`** — every anonymous-facing surface: the OG image route, the crawler HTML (`getSocialPreview`), and both MCP tools when the board is a published one. A published board must be published; a shared file must currently be shared via link (`isFileAnonymouslyViewable`). This is the only thing keeping a private board's thumbnail off the public internet, and it is re-applied per request rather than inferred from what is in R2 — necessarily, since nothing deletes an image when a board stops being public. +- **`access: 'render'`** — the queue consumer, the render page's snapshot route, and the MCP tools once `hasReadAccessToFile` has admitted the caller for that file. Requires only that the board exists, is not deleted, is not a test file (`isFileRenderable`), and has persisted content. + + On the MCP path the per-user check is what stands in front of `render`, and it runs in the worker before the token is minted rather than at snapshot-read time. The snapshot route re-checks renderability but not the user, because the job carries no `userId` — so a caller who loses access to a board mid-capture still completes it. The exposure is bounded by the 60s token TTL and by the record below; signing a `userId` into the job and re-checking it on read would close it, at the cost of a database round trip on the render path. + +Three gates moved together, and all three had to: the durable object's edit trigger (which skipped anything not `shared`/`unknown`), the consumer's `resolveThumbnailBoard`, and `getThumbnailSnapshot`'s read. Relaxing fewer would have produced enqueues that were dropped downstream, or captures whose render page 404'd. + +The durable object now skips only two states, and neither is about privacy: `legacy` (not an app file, so no board identity to render) and `deleted` (nothing worth depicting). `shared`, `private` and `unknown` all render. + +**What this costs.** `GET /api/app/thumbnail-render/snapshot` previously refused anything not publicly viewable, so a leaked or forged render token exposed nothing that was not already public. It now serves a **private** board's full document — every shape. That makes the token load-bearing in a way it was not before, so two things guard it. + +`THUMBNAIL_RENDER_TOKEN_TTL_MS` is **60 seconds**, down from five minutes. Sized against what the token is actually for: the render page fetches the snapshot in its loader, seconds after navigation, and nothing touches the token afterwards — settle, `toImage` and capture all run without it. So the window has to cover browser start plus navigation plus bundle load, not the whole render. The thing that would break a short TTL is not a slow render but Browser Run _queueing_ before the browser starts (new instances are limited to 1/second); at current volume that is three orders of magnitude away. + +**A signature alone is no longer sufficient.** Every mint also records the token's hash in R2, and the route requires the record to be present (`recordMintedRenderToken` / `isMintedRenderToken`). A leaked `MCP_SCREENSHOT_TOKEN_SECRET` therefore stops being catastrophic: an attacker can forge signatures for any board, but without write access to our bucket the forgeries have no record and are refused before the board is read. The secret becomes one of two required factors rather than the sole authority over every private board's contents. + +Three details of that worth knowing: + +- **Only `render` jobs are recorded.** The access level rides inside the signed token: the MCP tool mints `public` for a published board and `render` for a file its caller may see, and the OG pipeline always mints `render`. A `public` job renders a board anyone could already fetch, so a forged token for one grants nothing and a record would buy no security. +- **Keyed per capture**, so records overwrite in place and the space stays bounded by content rather than by traffic. Nothing accumulates, so there is no lifecycle rule to add, and none must ever be added to this bucket. + + What counts as one capture differs by surface, because their concurrency does. The OG pipeline is single-flighted per board by the `.pending` marker, so its renders never overlap and one key per board is right (`render-tokens/{kind}/{slug}/og`) — a board's newest mint invalidating an older in-flight token is then intended, a fresher render superseding one already running. + + The MCP tool has no pending marker, and its per-board limiter deliberately allows two cache-missing captures a minute, so **two captures of one board would share a per-board key and invalidate each other** — as would an edit-triggered render landing during a capture. The loser fails its snapshot fetch with a 403, which surfaces as a generic `browser_failed`, indistinguishable from a real browser crash. So an MCP screenshot is keyed by what it draws: `render-tokens/{kind}/{slug}/mcp/screenshot/{theme}/{pageId}/{shapeSetDigest}`. + + A **measure** render has no content to key on — it exports nothing, and every measure of a page mints an identical job down to the hardcoded light theme — so it is keyed by its own token instead (`…/mcp/measure/{tokenHash}`). It has to be: the clustering tools all measure before they can do anything, so any two of `get_page_info`, `get_cluster_info` and `get_cluster_screenshot` running against one page would otherwise collide, which is the ordinary agent pattern rather than a rare race. A per-token key is the one thing here that would accumulate, so the measure deletes its own record when it finishes (`deleteMintedRenderToken`). + + Two residuals worth knowing. The _same_ MCP capture asked for twice at once still collides, and the later mint wins — the OG case again, one image rendered twice, costing a retry rather than a wrong result. And a token minted before the `surface` field existed is read as `og` and falls back to the old un-namespaced key, which is what keeps renders in flight across that deploy from 403ing on a record that was never going to be there. + +- **Records are not deleted after a capture.** Expiry lives in the signed `exp` and is checked before the record is ever consulted, so a leftover record cannot extend a token's life. Deleting would tighten the window from `exp` to the render's duration — worth nothing against an attacker who cannot get a record written at all, and it would cost a `finally` and a third state to reason about. +- **A hash, in `customMetadata`.** The bucket never holds a usable credential, and checking one is a `head` rather than a `get`. + +When `THUMBNAILS` is unbound (local dev, tests) the check is skipped, which degrades to signature-only verification — the level this replaces, not a hole beneath it. Every deployed environment binds it. + +Still worth doing when the authenticated retrieval endpoint lands: serving the render page as inline `html` with the records embedded would remove this public route altogether. + +Cost in Browser Run terms is not the constraint. The sizing here assumed ~30% of edited boards are link-shared, so rendering all of them is roughly 3.3x the previous volume — about 3 renders/min against measured production traffic of ~1/min and an account limit of 60/min. + +### One-time ops setup + +Before the first deploy of this feature: + +1. Create the R2 buckets: + + ```bash + wrangler r2 bucket create thumbnails-preview + wrangler r2 bucket create thumbnails + wrangler r2 bucket create dotcom-mcp-data-preview + wrangler r2 bucket create dotcom-mcp-data + ``` + +2. Add the expiration rule to the MCP data buckets only (30 days is a starting point — these are a regenerable cache, so the only cost of expiring one is a re-render the next time an agent asks for that exact board version): + + ```bash + wrangler r2 bucket lifecycle add dotcom-mcp-data-preview expire-screenshots --expire-days 30 -y + wrangler r2 bucket lifecycle add dotcom-mcp-data expire-screenshots --expire-days 30 -y + ``` + + Verify with `wrangler r2 bucket lifecycle list dotcom-mcp-data`. Do **not** add an equivalent rule to `thumbnails`. + +3. Enable Browser Rendering on the Cloudflare account (the `BROWSER` binding needs it) and add the `MCP_SCREENSHOT_TOKEN_SECRET` GitHub secret. Until the secret exists the deploy passes an empty string and the MCP tool returns a configuration error instead of failing the deploy. + +Migration note: MCP screenshots previously lived under `mcp/…` in the `thumbnails` bucket, where nothing ever deleted them. Those objects are now orphaned — the tool reads and writes the new bucket, and the version in the key means nothing will ever hit them again. Clear them out with a one-off prefix-scoped rule (`wrangler r2 bucket lifecycle add thumbnails expire-legacy-mcp mcp/ --expire-days 1 -y`, removed once the prefix is empty) or by deleting the `mcp/` folder from the dashboard. + +Second migration note: the OG key dropped its `{w}x{h}` segment, so anything already written as `og/{kind}/{slug}/1200x630/{theme}.png` is orphaned too. Bounded and small — one object per board ever rendered before this deploy, and each board rewrites at its new key on the next publish or edit — but nothing will read or overwrite the old ones, and this bucket has no lifecycle rule to sweep them. A prefix rule cannot match a middle segment, so clear them by listing and deleting the `og/` keys containing `/1200x630/`, or leave them and accept a fixed one-off cost. Do **not** reach for a lifecycle rule on `thumbnails` to do it. + +Third migration note: the MCP buckets were originally named `mcp-screenshots-preview` / `mcp-screenshots`, after the one artifact they held rather than the surface that writes them. R2 has no rename, so the names above are fresh buckets. Nothing needs copying — the preview bucket was created but never written to, and the production one was never created — so the old preview bucket can simply be deleted (`wrangler r2 bucket delete mcp-screenshots-preview`) once this config is deployed. + +## Local development + +Start the dotcom app from the repo root: + +```bash +yarn dev-app +``` + +The dev-only fixture page renders allowlisted example snapshots without a worker, published file, or token: + +``` +/dev/browser-run-thumbnail?fixture=layer-panel&x=340&y=120&z=0.82&width=1200&height=630&theme=dark +``` + +Capture it locally without Cloudflare credentials: + +```bash +yarn workspace dotcom browser-run-thumbnail \ + --mode local \ + --base-url http://127.0.0.1:3000 \ + --fixture snapshot-example \ + --output tmp/browser-run-thumbnail/local-thumbnail.png +``` + +To capture through real Browser Run, use a preview/dev deployment or a tunnel (Browser Run cannot reach `127.0.0.1`): + +```bash +CLOUDFLARE_ACCOUNT_ID=... \ +CLOUDFLARE_API_TOKEN=... \ +yarn workspace dotcom browser-run-thumbnail \ + --mode browser-run \ + --base-url https://your-dev-or-preview-origin.example \ + --fixture layer-panel \ + --output tmp/browser-run-thumbnail/browser-run-thumbnail.png +``` + +When tunnelling with Vite's host checks, start the client with: + +```bash +VITE_ALLOWED_HOSTS=your-tunnel-host.example yarn workspace dotcom exec vite dev --host 127.0.0.1 --port 3000 --strictPort +``` + +### The local screenshot service + +The full worker render path works under `yarn dev-app`: `POST /app/mcp` with `tools/call` returns a real PNG, and the OG image route renders through its queue consumer the same way. Both go through the render page and the token-gated snapshot endpoint exactly as production does — the only substitution is the browser. + +Local dev cannot reach Browser Run for two independent reasons. `[env.dev.browser]` is deliberately NOT marked `remote = true`, because a remote binding makes plain `wrangler dev` require a `CLOUDFLARE_API_TOKEN` that the credential-free process-compose stack does not have; and `[env.dev]` pins `compatibility_date` to `2025-06-05`, older than the `2026-03-24` `quickAction` needs, because that date is newer than the workerd bundled with our pinned wrangler. The second reason applies to `wrangler dev --remote` too, since it deploys with the same `[env.dev]` config, and dev's `MCP_SCREENSHOT_RENDER_ORIGIN` is unreachable from Cloudflare regardless. + +So instead of a browser binding, `[env.dev.vars]` sets `LOCAL_SCREENSHOT_SERVICE_URL` to the client dev server's `/__screenshot` endpoint. The worker isolate cannot drive a browser, but the dev server is a Node process that already depends on Playwright, so a dev-only vite plugin (`scripts/vite-thumbnail-screenshot-plugin.ts`, `apply: 'serve'`, so it cannot exist in a built client) launches Chromium once and screenshots the render page on request. The worker sends it the same request body it would send Browser Run's Quick Action, so the wait strategy, capture target, and timeout come from `getThumbnailScreenshotRequestBody` for both and cannot drift. The plugin will only ever open `THUMBNAIL_RENDER_PATH` on its own origin — it discards the caller's host and fixes the path, so it cannot be pointed at an arbitrary URL. + +This is not Browser Run: different Chrome build, different flags, no billing headers. It makes the local path work, and it is not evidence that a render will succeed in production — a preview deploy is still the check for anything Browser Run-specific, such as font availability in Cloudflare's fleet. + +## MCP tools + +```ts +search_boards({ + query?: string, // words to match against board names, ANDed, case-insensitive. omit to list the newest boards + cursor?: string, // the nextCursor from a previous result, replayed with the same query. omit for the first page +}) +// → { boardCount: number, nextCursor?: string, boards: { boardId: string, name: string, createdAt: string, updatedAt: string, source: 'owned' | 'workspace', workspaceName?: string }[] } + +get_board_info({ + boardId: string, +}) +// → { name: string | null, pageCount: number, pages: { index: number, id: string, name: string, hasContent: boolean }[] } + +get_page_info({ + boardId: string, + page?: number | string, // 0-based page index or stable page id (see get_board_info). default 0 +}) +// → { name: string, clusterCount: number, clusters: { id: string, label: string, keywords: string[], numberOfShapes: number }[] } + +get_cluster_info({ + boardId: string, + page?: number | string, + clusterId: string, +}) +// → the cluster's label, keywords, and full shape records (rich text flattened to props.text) + +get_cluster_screenshot({ + boardId: string, + page?: number | string, + clusterIds: string | string[], // one or more cluster ids from get_page_info + theme?: 'light' | 'dark', // default 'light' +}) +// → text (page name) + a 1200x630 content-fit PNG of those clusters +``` + +`search_boards` covers the same set the other four will open: boards in the caller's own workspace, boards owned by a workspace they can access files in, and boards shared with them by link that they have opened. That is the scope `hasReadAccessToFile` admits, and the relationship only runs one way — everything search returns must be a board the other tools will open, or a model finds a board and cannot interpret being refused it. A published board stays reachable by id; it is simply not discoverable by name. + +**Two reads, ordered by when a board reached the caller.** The access gate has two arms with nothing in common but their answer, so there are two queries, merged in JS through `compareBoardSearchOrder` rather than unioned — that comparator is the single statement of this ordering and the eval harness already pages fixtures with it, so a `UNION`'s own `ORDER BY` would be a third copy to drift. They are issued sequentially because the pool is `max: 1`. + +Workspace-owned boards are found by `file."owningGroupId"` — deliberately not through `group_file`, since `hasReadAccessToFile` reads `owningGroupId`, and `group_file` is list membership rather than ownership: `deleteWorkspace` removes those rows by design, and a file can outlive its links (see #10837). A missing link must never be able to hide a board you own. Link-shared boards are found the only way they can be: opening one writes a `group_file` row into the opener's **home** group while the file stays owned elsewhere, which `039_tighten_group_file_association.sql` permits and no other cross-workspace link, and unsharing deletes those rows (`034_fix_unshare_group_file_cleanup.sql`) so the guest set cannot outlive the access that justifies it. + +**The sort key is arrival, not creation**, and each read takes it from wherever that lives. For a workspace board it is `file."createdAt"` — not a proxy for arrival but the same value, since `createFile` writes one timestamp into the file row and its `group_file` row alike. For a shared board it is `group_file."createdAt"`, the moment the link was opened. Both are immutable, which is what a keyset cursor needs. The workspace side is index-served by `052_file_search_index.sql`; the shared side is not yet — see below. That ordering is the point — under a creation sort, a board shared with you this morning but made in 2023 lands below everything you have made since. The one case the two diverge is a board _moved_ between workspaces, which keeps its original creation time rather than sorting by the move. Results report `addedAt` alongside `createdAt` so a model can see which is which, and `source` says how the caller reaches each board — `owned`, `workspace`, or `shared`. `shared` is deliberately not a flavour of `workspace`: the caller is not a member of the workspace that owns a link-shared board, so the owning workspace's name is withheld from those rows rather than handed to somebody who was given a link rather than a seat. + +**The shared read has no index yet, deliberately.** Both the column it filters on and the one it orders by are on `group_file`, so a single index serves it — there just isn't one, and until there is it fetches every guest link the caller has and sorts them. Measured at production's shape against the heaviest guest-link count (233): 879 buffers, against **89 buffers and 22 rows** with an index on `group_file("groupId", "createdAt" DESC)`. Both are under two milliseconds, and the cost only stops scaling with link count once the index exists. + +That index ships as its own deploy, and the reason is a deadlock rather than caution. The runner holds one transaction over every pending migration, so a run building indexes on both `file` and `group_file` holds the first while waiting for the second — and `moveFileToWorkspace` takes those two tables in the opposite order (`group_file` delete, then the `file` update). Reproduced on PG16: `40P01`, with the migration as the victim, so the deploy fails. No lock ordering fixes it, because the app is inconsistent with itself — `createFile` takes `file` then `group_file`. A run that locks only one of the two cannot be in the cycle at all, which is what separate deploys buy. So: `052` ships with the tool, the `group_file` index follows on its own. + +It returns 20 boards to a page, ordered **newest-created first** — `file."createdAt"` descending, `file.id COLLATE "C"` descending to break ties — read from the `file` table alone, and pages with a keyset cursor rather than an offset, since an insert above the cursor shifts every later row and an offset would re-serve one board and skip another at the boundary. + +The seek is written as a row comparison, `(file."createdAt", file.id COLLATE "C") < (:createdAt, :id)`, and that form is load-bearing: Postgres keeps the logically equivalent `createdAt < :c OR (createdAt = :c AND id < :i)` as a filter, so a late page walks every row above the cursor and discards it — page 500 of a 10k-board workspace read 9,901 rows to return 21, against 5 for the row comparison, which PG16 truncates to the leading indexed column and turns into `createdAt <= :c` as an index condition. + +The cursor also carries the query that minted it, and a cursor replayed under a different query is refused rather than honoured. Without that, a model that pages and then narrows its query while passing the cursor on continues from row 20 of a set it never saw the start of, with nothing in the result to say so. + +A board with no name comes back with `name` empty rather than a stand-in title. tldraw.com titles an unnamed board by its creation date, formatted in the viewer's locale and timezone — neither of which a worker has — so any name invented here would be one the caller cannot see on their own screen, and one no query could match, since the column holds `''`. + +`createdAt` rather than the obvious-looking `updatedAt`, and this is the part worth not undoing: **a keyset cursor cannot survive a mutating sort key.** A row whose key changes can move from below the cursor (not yet served) to above it (already passed) and then appear on no page at all. `005_update_file_trigger.sql` bumps `file."updatedAt"` on any column change, so under `updatedAt` a board edited while an agent is paging silently vanishes from its results. `createdAt` is written once — by the `createFile` mutator, and it sits in `immutableColumns.file`, with no trigger touching it — so only inserts and deletes move the set, which is exactly what a keyset handles. `updatedAt` is still returned per board, because "when was this last changed" is a thing a model wants; it is simply not the order. + +Earlier revisions ordered by per-user recency, joining `file_state`. That join was the performance problem — a sort key reached through a left join and a `COALESCE` can never be index-ordered — and it is also the join that would have quietly widened the scope, since a `file_state` row exists for anyone who has opened a link-shared board. It is gone; don't reintroduce it. + +Search spends no Browser Run, so it sits outside the rate limits below — but unlike `get_board_info`, it is not simply unlimited: it has a budget of its own, because the cost it drives is Postgres rather than a browser session. Paging is now **index-bounded**: `file("owningGroupId", "createdAt" DESC)` (`052_file_search_index.sql`) serves the access filter and the ordering together, so each workspace's page is read in index order and stops — `LIMIT 21` saves work, not just transfer. Nothing indexes `name ILIKE '%…%'`, so a search term is still a filter applied to rows the index scan walks past: cost then scales with how far down the caller's boards the matches sit, and a term matching nothing walks their whole set. + +**That part is behind a switch.** `MCP_SEARCH_NAME_MATCHING_ENABLED` gates name matching per environment — `"true"` on staging, `"false"` on production while the scan above is still being watched, and unset reads as on so previews, local dev and tests keep the full tool. Off, `search_boards` becomes a listing tool: it advertises no `query` and refuses one, rather than accepting a query and ignoring it. That distinction is the point — a model that asked for "roadmap" and received the account's twenty most recent boards would read them as twenty matches and open one. + +**Why it took two edits, and what still sorts.** The index is usable only because the access predicate is a single equality, which it became when [#10391](https://github.com/tldraw/tldraw/pull/10391) removed the last user-owns-file paths. While the predicate was `file."ownerId" = :caller OR file."owningGroupId" IN (:groups)`, Postgres answered the top-level `OR` with a `BitmapOr`, whose output has no order, so the plan was a bitmap heap scan of everything in scope followed by a top-N sort whatever indexes existed. Measured on Postgres 16.14 against the real DDL and an 8000-board workspace, and re-measured after the merge: with the `OR` arm present the planner materialises ~8010 rows and top-N sorts them, and ignores this index entirely in favour of the existing `file_owning_group_id_idx`; with the arm gone and one workspace, the same query is an index scan plus incremental sort reading 22 rows for a 21-row page. **So don't reintroduce a top-level `OR` in the access predicate** — it silently un-indexes the ordering rather than failing anything. A caller in more than one workspace would produce `owningGroupId = ANY(:groups)`, which is not that single equality — a btree scan driven by an array key is not order-preserving before Postgres 17, and production runs 16 — so that case seq-scanned every board in scope and top-N sorted it: 30,000 rows for the first 21, measured across three 10k-board workspaces. The query therefore asks each workspace for its own page and merges them, `unnest(:groups) CROSS JOIN LATERAL (… LIMIT 21)`, which reads 63 rows for the same answer. The merge is correct because each arm takes a whole page: the global top 21 cannot contain a row missing from every workspace's own top 21. Every filter has to stay inside the arm for the same reason — one applied after the arm's `LIMIT` could empty a page that had more rows to give. The index deliberately stops at `createdAt` and leaves the `file.id COLLATE "C"` tiebreak to the incremental sort: the primary key already makes `id` unique, and ties on `createdAt` are rare. + +**Its own limiter, not the Browser Run one.** `MCP_SERVER_SEARCH_RATE_LIMITER` bounds `search_boards` at 60 calls a minute per account (`MCP_SEARCH_PER_USER_RATE_LIMIT`), separately from the per-account Browser Run budget — counting search against that one would make the number `config.ts` documents for it untrue, and would inherit a limit chosen for a different cost. + +60 rather than something tighter because **paging is what spends this, not abuse.** The page size is fixed at 20, so a caller in an 8000-board workspace needs 400 calls to walk it, and an agent that reformulates a failed query spends a few more. A refusal mid-page has no good recovery either: a model cannot wait, so it retries or reports a partial answer as a complete one, which is worse than the load it prevents. Against that, the worst measured call is ~9ms of database time, so 60 a minute is well under a second of database time per caller per minute. The refusal is written to the screenshot ledger as `rate_limited_search` even though successful searches are not — that ledger is the panel answering "who is being turned away", and a limit nobody can watch firing is the thing the per-user budget already had to be fixed for once. + +What this does **not** bound is many accounts looping at once; that needs a global search limiter alongside it, the way `MCP_SERVER_BROWSER_RATE_LIMITER` sits under the per-user screenshot budget. Deliberately left until there is traffic to size it against, with `postgres_client_connect` volume from this surface as the signal. + +Every tool accepts the id of a tldraw.com board the authenticated caller can see: the `:slug` of a file URL (`https://www.tldraw.com/f/:slug`) they own, can reach through its owning group, or that is shared via link — or the `:slug` of a published board URL (`https://www.tldraw.com/p/:slug`). The id is resolved as a file first and a published slug second. Deleted files and test files are refused, as is any board the caller has no access to; all of these answer the same "no board was found with this id" as an id that names nothing at all. + +Each `get_cluster_screenshot` call renders one image, so an agent typically calls `get_board_info` once to enumerate pages (using `hasContent` to skip blank ones), `get_page_info` to list a page's clusters, then screenshots the clusters it cares about — one, or several together to see how they sit relative to each other. That whole drill-down costs **one measure render**, not one per call: the first tool to cluster a page stores its clustering and the rest read it (see below). A screenshot on top of that costs one capture, or none if the PNG cache already holds those shapes. + +The screenshot layer lives in the dotcom sync worker rather than the interactive `apps/mcp-app` canvas worker because it needs real tldraw.com published-file resolution and storage, not a live editor bridge. + +### The cluster index + +Three of the four tools have to cluster a page before they can answer anything, and clustering needs two things only an editor can produce: where each shape sits, and what its `ShapeUtil.getText` reports. Getting them means a **measure render** — a full Browser Run session, the same cost as a screenshot. That used to happen on every call, so the documented drill-down (`get_page_info` → `get_cluster_info` → `get_cluster_screenshot`) spent three of them on one page, and `get_cluster_info` — the tool an agent calls most, often several times over the clusters one page listed — paid full price every time. + +The answer, though, only moves when the board's content moves. So the first tool to measure a page reduces the render's output to a **cluster index** and stores it; every later call for the same content is served from that. + +What is stored, per page (`PageClusterIndex` in `boardTools.ts`): each cluster's id, label, keywords and the ids of its shapes, plus the plain text the editor reported for the shapes that have any. Bounds are deliberately **not** kept — they exist to decide which atoms merge, and that decision is already baked into the cluster membership. The text is kept because it is the one thing that cannot be re-derived: `getShapeText` can approximate it from the stored record, and dropping the editor's answer would quietly change what `get_cluster_info` reports for exactly the shapes the fallback handles worst. Shape _records_ are not stored either; they come from the room snapshot, which every tool call loads anyway. + +**Where it lives: the file's own durable object** (`TLFileDurableObject`, table `mcp_cluster_index`, statements in `mcpClusterIndexStorage.ts`), addressed by `ResolvedThumbnailBoard.fileId` — the slug for a shared file, the parent file for a published board. This is the only durable object hop in the MCP request path; the tools otherwise read the persisted snapshot straight out of R2 and never talk to the object that wrote it. It sits there rather than in R2 beside the PNGs because it is content derived from the room, and the room is what that object owns: the row is strongly consistent, it dies with the file, and it needs no expiry policy. + +**Invalidation is the cache key**, which carries the same content version the PNG cache uses — the persisted snapshot's R2 etag for a shared file, `lastPublished` for a published board. An edit or a republish rotates it and the stored index stops matching. The primary key is `(kind, pageId)` with the version held as a _value_, so writing a page's index for new content replaces the row for the old content rather than adding to it: a file's cache is bounded by its page count (and enumeration stops at `MAX_THUMBNAIL_PAGES`), not by its edit history. There is no slug in the key — the object is already the board's file, and a published slug can be rotated, which would strand a row nothing ever replaces. `kind` stays, because one file is two boards — the live shared file and the frozen published snapshot — which cluster differently as soon as the two have drifted apart. + +**Every path through it falls back to measuring.** A miss, a row from a build whose `CLUSTER_INDEX_FORMAT_VERSION` differs, a payload that does not parse, an index naming a shape that is not on the page, a durable object that throws — all of them are a cache miss, which costs one render and is always correct. That includes the case where `get_page_info` was simply never called: `get_cluster_info` on a cold board measures, stores, and answers. Two limits bound what the cache itself can cost: a page whose index would exceed `MAX_CLUSTER_INDEX_LENGTH` (256K UTF-16 units) is never stored and keeps measuring, and a write that fails is reported to Sentry but never fails the tool that just spent the render. + +**Bump `CLUSTER_INDEX_FORMAT_VERSION` when clustering or labelling changes**, not only when the stored shape does. The cache key rotates on edits, not on deploys, so nothing else stops a board that has not been touched since the change from serving groupings the current code would not produce. + +The limiters follow the spend: the global Browser Run cap is consulted on the miss path only, since a call served from the index spends nothing to meter. The per-account budget still counts every call, cache hit or not — it is a ceiling on calls rather than on captures, so a caller looping over hits stays bounded. + +### Protocol versions + +`POST /app/mcp` serves two revisions of MCP at once: `2026-07-28` and `2025-11-25`. `2024-11-05` is no longer served — a client that asks for it during `initialize` is answered with `2025-11-25`, which is that handshake's own way of declining a version, and one that names it in an `MCP-Protocol-Version` header is rejected with `-32022` and the list of versions that are on offer. + +The split matters because `2026-07-28` removed the handshake. There is no `initialize`, no session, and no `Mcp-Session-Id`: every request carries its own protocol version, in both the `MCP-Protocol-Version` header and `params._meta`. That header is what picks the era for a request, and the choice reaches only the response envelope and a few status codes — the tools underneath are the same code either way. Modern results carry `resultType: "complete"` and name the server in `_meta`; `tools/list` also carries `ttlMs` and `cacheScope` so clients can cache the tool list instead of re-fetching it. Legacy results carry none of that. + +The modern transport also mirrors the routing-relevant body fields into headers — `Mcp-Method` on everything, `Mcp-Name` on `tools/call` — so that a gateway can dispatch without parsing the body. Where a header and the body disagree the request is refused with `-32020` rather than one of them being picked, because the disagreement is the whole problem: one value is what would run, the other is what the network already made its decisions on. + +A missing version header is served as legacy rather than refused. Clients from `2025-06-18` on are supposed to send it and plenty don't, and a request without it is wire-identical to one with it, so rejecting would break working clients over a field this server doesn't need. + +`server/discover` is the one call that answers whatever version it is asked under, including a version we don't serve: it exists so a client can find out what we speak, and gating that on already speaking it would be circular. `GET` and `DELETE` return 405 — they addressed the old standalone SSE stream and session teardown, which this server never offered and the current revision has removed. + +### Authentication on the MCP server + +Every request to `POST /api/app/mcp` needs an OAuth 2.1 bearer token, checked before the JSON-RPC body is read — `initialize` included, because MCP's flow expects the unauthenticated call to answer `401` with a pointer to the metadata, which is how a client learns it needs to sign the user in at all. There is no anonymous tier: the endpoint used to serve any caller that named a public board, and requiring a token retires that deliberately. What it buys is per-account rate limits instead of per-IP, attributable Browser Run spend, and the per-user board access check that lets the tools reach a caller's own private boards. + +The pieces, all in `mcpAuth.ts`: + +- **Clerk is the authorization server; this worker is only a resource server.** It never issues a token. sync-worker was already a Clerk consumer (`@clerk/backend`, `CLERK_SECRET_KEY`, `getAuth.ts`), which is what made this the shorter path — a standalone worker with no existing identity story would more likely want its own authorization server. +- **The authorization server is derived from `CLERK_PUBLISHABLE_KEY`, not configured separately.** The same key drives token verification, so the instance clients are sent to and the instance whose tokens we accept cannot drift apart. Two vars could, and the symptom would be every client completing sign-in and then being refused, with nothing in the logs to distinguish it from a bad token. `MCP_OAUTH_AUTHORIZATION_SERVER` overrides it if that is ever wrong. +- **Protected resource metadata is served at the origin** (`/.well-known/oauth-protected-resource/api/app/mcp`), per RFC 9728. This is why the worker has a **second route pattern** in `wrangler.toml`: it is otherwise routed only `www.tldraw.com/api/*`, and a client that fetches the metadata URL and gets the SPA's `index.html` never discovers the authorization server. That failure is silent on our side — it produces no request to log. The same trap exists in local dev, where the vite proxy needs the path and the preview server's SPA fallback needs to skip it. +- **Tokens are verified against the Clerk instance's JWKS, with `jose`.** Signature, issuer, lifetime (`requiredClaims: ['exp']`, since jose enforces only the claims it is told to) and token type. Not `@clerk/backend`'s `verifyToken`, which verifies Clerk _session_ tokens and refuses an OAuth access token on its header alone (`Invalid JWT type "at+jwt". Expected "JWT"`) — RFC 9068 requires `at+jwt` of an access token, so Clerk's authorization server and its own backend SDK disagree, and a resource server has to do this itself. [#10005](https://github.com/tldraw/tldraw/issues/10005) tracks the v2 SDK, which handles both. +- **`typ: at+jwt` is load-bearing, not pedantry.** It is the only thing separating an OAuth access token from a Clerk _session_ JWT: Clerk stamps no `aud` on either, so nothing can tell them apart by audience. A session token — `typ: JWT` — would otherwise be a valid bearer token here, which would make an ordinary tldraw.com website credential enough to drive this server and the consent step an agent walks a user through decoration. +- **There is no audience check, and its absence is deliberate.** RFC 8707 would bind a token to the resource it was minted for via `aud`, so a token a user granted to somebody else's MCP server could not be replayed here. Clerk does not implement it — it stamps no `aud` whether or not the client sends a `resource` parameter, so there is nothing to compare. What closes the hole instead lives on the authorization server, where the client registry is: with dynamic client registration off, **Only allow pre-registered clients to connect** on and **Block implicitly allowed clients** on — all three confirmed on the instance — a client nobody approved cannot obtain a token for our users at all. **That registry is the whole of the client-authorization story, it is dashboard state, and it is invisible from this repository** — no config entry, no test, no alarm. If any of it is turned off, a self-registered client can call this endpoint with a token its user consented to for something else entirely. Treat it as a standing invariant to confirm before enabling and to re-confirm after any Clerk change; a `client_id` allowlist in `mcpAuth.ts` is the belt to those braces if it is ever wanted, since the claim is on every token. +- **The endpoint and its metadata answer their own CORS**, and are registered ahead of the worker's origin allowlist. That allowlist protects _cookie_-authenticated routes; nothing here reads a cookie. A browser-context client — MCP Inspector on `localhost:6274`, a web connector fetching directly — is on no allowlist and never will be, and behind the check it got a bare `403 Not allowed` with no CORS headers, indistinguishable from "there is no MCP server here". `WWW-Authenticate` is exposed, since a client that cannot read the `resource_metadata` pointer cannot start the sign-in the `401` is inviting. Clients that send no `Origin` at all (Claude Desktop, `mcp-remote`, Cursor) were always fine, which is why this is easy to miss: testing with one of them proves nothing about the others. +- **Refusals are on their own event.** `mcp_server_auth_refusal` carries a bounded `reason` (`no_token`, `invalid_token`, `unconfigured`, `not_allowlisted`) and the client family. Nothing else records them — `mcp_server_tool_call` is written by the tools/call dispatcher, which a refused request never reaches — and during a flag-gated rollout "not signed in" and "signed in and not on the list" are the two numbers worth watching, and they call for entirely different responses. +- **The protocol version floor moved up, and `2024-11-05` was dropped.** That drop is what made any of this possible: MCP had no authorization flow before `2025-03-26`, so a client held to the old version has no conformant way to obtain the token every request now needs. The server now serves `2026-07-28` and `2025-11-25` side by side — see [Protocol versions](#protocol-versions) for how an era is picked and what it changes. +- **`mcp_server_access` decides who is let in.** An authenticated user the flag does not name gets `403` — not `401`, which would have clients loop through sign-in, and not `404`, which would hide an endpoint whose existence is already public in the discovery metadata. The flag is an **allowlist** type, added for this: `FeatureFlagValue` previously offered only boolean and percentage, and a percentage is a pseudo-random bucket (`hash(userId + flagName) < percentage`) that yields _a_ subset of the right size but never _the_ subset someone picked. The eventual bar is any signed-in tldraw.com user; the allowlist is the dial on the way there, not a narrower entitlement. + +**One thing to confirm before this is switched on.** Verification validates a **JWT** against the instance's JWKS. If the Clerk OAuth authorization server is configured to issue opaque access tokens instead, they cannot be verified this way at all — that needs `idPOAuthAccessToken.verifySecret`, which arrived in `@clerk/backend` v2, and this worker pins 1.23.7, where there is no OAuth token API. Clerk's **Generate access tokens as JWTs** toggle is on by default and is what keeps this path valid; check it when the instance is set up. + +Client registration is the other thing to configure. Clients identify themselves with Client ID Metadata Documents (CIMD): a client's id is an HTTPS URL to a metadata document its vendor hosts, so there is no registration endpoint. Clerk supports this (beta, enabled per account) through **Advertise CIMD support**, **Only allow pre-registered clients to connect**, and **Block implicitly allowed clients** — all three on, with the clients we verify allowlisted. The last two are what make the first safe: advertising CIMD alone admits any client that connects, and the third revokes the ones Clerk auto-allowed on first connect. We don't support dynamic client registration because of security concerns; a client that requires it isn't supported until it updates. [`mcp-server-auth.md`](./mcp-server-auth.md) has the reasoning. + +## Remaining follow-up work + +- **Move thumbnail rendering onto its own queue and cap it with `max_concurrency`.** This is the only mechanism in this stack that bounds total render rate: per-board rules cannot (total is `boards × rate`), and Cloudflare's rate limit bindings apply per location. Because the consumer captures one screenshot at a time, the arithmetic is exact — at the measured 6.3s capture, `renders/min ≈ 9.6 × max_concurrency`, so 4 is ~38/min and 5 is ~48/min. Its real merit is the failure mode: a capped consumer defers, where an over-limit Browser Run call fails and retries three times, so a cap converts overload into queue depth rather than amplification. + + Two prerequisites, which are why this is a follow-up rather than a config line. **The queue has to be split first** — `tldraw-multiplayer-queue` also carries `asset-upload`, and `max_concurrency` is a per-consumer setting, so capping the shared queue would throttle uploads too. And **`OG_PENDING_MARKER_TTL_MS` has to be re-derived from the cap**: it is sized against a job's retry chain (~3.75 minutes) on the assumption that queue latency is negligible, which is true only while nothing throttles. Once depth can build, a message can wait longer than its marker lives, the marker lapses, the next edit enqueues a second job for the same board, and the two clobber each other's per-board render token record — adding load exactly when the queue is already behind. Either refresh the marker when the job starts or derive its TTL from the cap. + +- Schedule `fetch-screenshot-metrics.ts --check` somewhere (cron CI job or an external monitor) and point a dashboard at the SQL queries above; the script and queries exist, the scheduling is an ops decision. +- Shared files render the last persisted room snapshot from R2, which can lag in-memory edits by the persist debounce. If near-real-time accuracy is ever required, add a `getCurrentSnapshot` RPC on `TLFileDurableObject` (modeled on `onDownloadTldr`) instead of reading R2. +- Keep current-viewport screenshots, arbitrary shape selectors, and arbitrary URLs out of the MCP scope; boards stay limited to what the authenticated caller could already open in a browser. + +## System map + +The pixels come from `editor.toImage` on the render page. The worker calls the Browser Rendering `/screenshot` Quick Action through `env.BROWSER.quickAction`, which navigates the render page, waits for either terminal marker, and captures the success-only `body[data-thumbnail-ready="true"]` element (so `data-thumbnail-error` returns a render failure immediately instead of waiting out the timeout). The render page renders one page, exports it with `editor.toImage`, and displays that PNG as a full-viewport `` — so the screenshot is the exact export. The screenshot response body is the PNG, which the worker writes to R2 and returns. No puppeteer, no API token, no page-side upload endpoint. + +```mermaid +flowchart TB + subgraph entry ["Entry points (sync worker)"] + SB["search_boards
(POST /api/app/mcp — Postgres only, no browser)"] + BI["get_board_info
(POST /api/app/mcp — no browser)"] + MCP["get_page_info / get_cluster_info /
get_cluster_screenshot
(POST /api/app/mcp — measure render + capture)"] + OGR["GET /api/app/social-preview/:prefix/:slug/image
(serves R2 cache only, never waits)"] + SP["GET /app/social-preview/:prefix/:slug
(crawler HTML: board name + og:image)"] + QC["Queue consumer
og-image-render (async refresh)"] + end + + subgraph warm ["Refresh triggers (ahead of the first crawler)"] + PUB["publish effect
(publishSnapshots.ts via TLFileEffectProcessor)"] + SPEC["persist on edit
(TLFileDurableObject,
debounced 60s, 5min max wait)"] + end + + SP -->|og:image references| OGR + PUB -->|reason: publish| QC + SPEC -->|reason: edit| QC + + SB --> PG[("Postgres: file
owned or workspace scope, newest created first")] + BI --> GATE["Resolve board + share gate
(published or link-shared only)"] + MCP --> GATE + QC --> GATE + + GATE --> SNAP2["Load room snapshot
(enumerate pages; board-info returns here)"] + SNAP2 -->|name, page list| BI + SNAP2 --> IDX{"Cluster index for this
page + content version?
(TLFileDurableObject SQL)"} + IDX -->|hit: no render| MCP + IDX -->|miss| TOKEN["Mint HMAC render token
(board identity, pageId, 60s expiry)"] + TOKEN --> BR["env.BROWSER.quickAction
(navigate → wait data-thumbnail-ready → PNG)"] + + BR --> PAGE["/__thumbnail-render (client render page)"] + PAGE --> SNAP["GET /api/app/thumbnail-render/snapshot
token → records + schema + render params"] + PAGE --> EXPORT["setCurrentPage(pageId) · editor.toImage()
content-fit · display as full-viewport img"] + EXPORT -->|screenshot captures the img| BR + + BR -->|measure bounds + text| IDX + BR -->|PNG bytes| WORKER["Worker writes R2 + returns image"] + WORKER --> R2[("THUMBNAILS bucket (og/… keys)
MCP_DATA_BUCKET (mcp/… keys,
expiring)")] + WORKER -->|image in hand| MCP + R2 -->|serve cached| OGR + R2 -->|serve cached| MCP +``` + +### Follow-up work + +The MCP/OG rework and the Browser Rendering binding migration described above are implemented. Since then: + +- Done: the board image endpoint is `GET /api/app/social-preview/:prefix/:slug/image` (worker route `/app/social-preview/:prefix/:slug/image`), so the crawler HTML and its image share one route family. +- Done: `GET /app/og-html/:kind/:slug` and its Vercel route are removed. `getSocialPreview` supersedes it (board name in the title, human bounce-back), which also fixed the live bug where crawler-UA in-app browsers (WhatsApp, Pinterest) bounced back with the bypass param fell through the og-html stub (no redirect) and never reached the board, and made `SOCIAL_PREVIEW_DISABLED` a complete kill switch. +- Done: the shared thumbnail dimension constants (default 1200x630, clamp 200-1600) live in `@tldraw/dotcom-shared`; the worker and the client render page both import them. + +Not doing: + +- The render token's explicit viewport (`x`/`y`/`z`, used when `camera` is omitted) stays, even though every surface mints `camera: 'content'` and nothing exercises it in production. It was briefly removed as unreachable and put back. The token payload is short-lived and has no stored state, so dropping it costs nothing to undo _on the worker side_ — but the render page ships with the client and the worker deploys separately, so bringing it back would mean landing the client's handling first and waiting for that deploy before the worker could send one. Keeping it holds that door open for the price of three ignored fields. +- `useThumbnailPageSize` stays in `thumbnail-render.tsx`. It is load-bearing for the production render page, not dev-only: the render page displays the export as a full-viewport `` and Browser Run takes a viewport screenshot of it, and the dotcom client has no global `body { margin: 0 }` reset (only `#root { width/height: 100% }` in `index.html`), so without the hook's `margin: 0` the browser's default 8px body margin would offset the image and the screenshot would show a white border and clip the bottom-right of every thumbnail. + +## Real thumbnails on first share + +Status: phases 2 and 3 are implemented and on; phase 1 is not shipping and phase 4 is still conditional. The mechanics of what shipped are documented above — "Default-image fallback" under the OG images section, "Keeping the thumbnail current" for the publish and edit triggers, and "Request limits" for why the render path is uncapped. This section keeps the rationale and the outstanding work. + +### Problem + +The first crawler to unfurl a board hits a cold OG-image cache and is sent to the generic tldraw image, and platforms cache that unfurl card on their side for days. So the first share of a board shows the logo rather than the board, and stays wrong long after the render lands seconds later. + +The card itself is valid — the crawler follows the redirect and gets a real image — so there is nothing to fix in how the miss is served. The only lever is how often a crawler finds nothing, which means making the thumbnail exist first. + +### Strategy + +Make the thumbnail exist before the first crawler arrives. No synchronous rendering on crawler paths; the pending marker and the queue stay load-bearing as dedupe. + +| Phase | Covers | Cost | Status | +| --------------------------- | ----------------------------------------------------- | -------------------------------------------------------------- | --------------------- | +| 2. Publish hook | explicit publish/republish, always fresh | negligible | done | +| 3. Render on every persist | the create → draw → share flow and revived old boards | ~1 render per editing session, +1 per 5min of unbroken editing | done | +| 4. Hop-1 warming (optional) | immediate shares, never-edited-again boards | negligible | published boards only | + +Phase 1 was a fallback that served the default image's bytes from the worker as a `200` rather than redirecting to them. It is not shipping: it existed to protect crawlers that do not follow an `og:image` redirect, and it bought a generic card either way. Phase 4 is what covers the case it aimed at — a board shared within the debounce window of its first edit. + +`getOgImage`'s stale-serve behaviour is the residual backstop: a board whose thumbnail is out of date still gets its own picture, on a short TTL, rather than the site-wide default. The general on-miss enqueue that used to sit alongside it is gone; what survives is phase 4 narrowed to the one case that needs it, a published board with no image at all, since that is the only kind with no second trigger to fall back on (see "OG images (queue-backed async rendering)"). + +### Beyond OG images + +This isn't just a crawler fix. The result is a board thumbnail that's reliably current in R2 — kept fresh by publish and edit triggers rather than rendered synchronously per request — for every shared board, not only the ones a crawler happens to hit. That's the same primitive other surfaces need: board previews in a folder or workspace view, for instance, which today would otherwise need their own render-on-demand path or ship without a real thumbnail. Those surfaces can just read the cached image. + +### Phase 0 — measure (still worth doing, no longer a gate) + +Pull two numbers from existing telemetry (`mcp_shared_board_screenshot` dataset via `internal/scripts/fetch-screenshot-metrics.ts`; `room_empty`/persist log events): + +- daily unique boards with contentful edit sessions +- daily unique boards receiving og-image fetches + +This was originally the gate on turning edit-triggered rendering on, because it sized the sampling percentage and the raised global cap. Neither exists now: rendering is uncapped and runs for every board. The numbers are still the ones to watch, but as observation rather than a precondition — they say what thumbnail rendering actually costs per day and whether Browser Rendering's account-level session limits are anywhere near being the binding constraint. + +### Full path, from first edit to first-share cache hit + +```mermaid +flowchart TB + EDIT["t=0 — a change lands
in TLFileDurableObject"] + EDIT --> PERSIST["t≤8s — persist tick advances the
document clock (persistToDatabase)"] + PERSIST --> DEBOUNCE{"scheduleOgRender
(push deadline out 60s —
THE rate control)"} + + DEBOUNCE -->|"more edits arrive first"| WAIT["alarm fires, sees a newer
deadline, re-arms (no render)"] + WAIT --> DEBOUNCE + DEBOUNCE -->|"editing settled, or
5min max wait hit"| GATE{"requestOgRenderForEdit
(share gate; fire-and-forget,
never blocks persistence)"} + + GATE -->|"file record not shared"| SKIP["skip"] + GATE -->|"shared or unknown"| MARK{"pending marker
already set?"} + + MARK -->|"yes"| DEDUPE["already_pending — no message
(single-flight, not a rate limit)"] + MARK -->|"no"| ENQ["write marker + enqueue
reason: edit"] + + ENQ --> QUEUE[("queue")] + + QUEUE --> CONSUMER["consumer delivery"] + CONSUMER -->|"board no longer viewable"| DROPV["drop + delete cached image"] + CONSUMER -->|"cached version already current"| ACK["ack without rendering
((board, version) idempotency)"] + CONSUMER -->|"otherwise"| RENDER["re-resolve → render CURRENT content
(no capacity check)"] + + RENDER --> R2[("THUMBNAILS R2
og/… key, version + createdAt")] + R2 --> SHARE["first share: crawler og:image
fetch is a cache hit"] +``` + +There are two constants to tune, both in `config.ts`, and the measurement in "What it costs" says they only work as a pair. `OG_RENDER_DEBOUNCE_MS` (60 seconds) sets how long a board must go quiet before its thumbnail is rendered — lower it for fresher thumbnails after short bursts, raise it to absorb more of a session into one render. `OG_RENDER_MAX_WAIT_MS` (5 minutes) caps how long a continuously edited board can go stale. Raising either one alone barely moves total spend, because a longer debounce merges bursts into sessions that then run into the max wait, trading a settle render for a max-wait one; raising both together is what actually reduces renders. `OG_PENDING_MARKER_TTL_MS` is not a dial: it is a single-flight guard and a crash ceiling, cleared as soon as a render lands. + +### Phase 4 (conditional) — hop-1 warming + +- `getBoardOgImageUrl` in `getSocialPreview.ts` already resolves the board; add: if no fresh cached image (one R2 `head` plus version compare, extracted from `getOgImage`'s check), `ctx.waitUntil(enqueueOgImageRender(env, board, { reason: 'crawler' }))`. Thread `ctx` into the route. This is the one thing that would put `crawler` back in use as a live reason. +- Ship only if phase-3 telemetry shows first-fetch misses are still meaningful (dormant never-edited boards, mostly — the delay that immediate sharers used to beat is gone). + +### Explicitly not doing + +- Synchronous wait in `getOgImage` — the queue's ~5s default batch linger means a short wait mostly misses, and the phases above remove the need. Revisit only with data showing otherwise. +- An `isEmpty`-based trigger — the `file.isEmpty` column is vestigial (written `true` at creation, never flipped by client or server), so there is no outbox-visible first-content transition. +- A DO-owned render single-flight — the advisory pending marker plus the consumer's version check is the accepted model and stays adequate at these volumes. +- A cap on thumbnail rendering — see "Request limits". Capping our own derived artifact only buys staler thumbnails; the caps belong on the MCP endpoint, which is the surface an outside caller can actually drive. + +### Metrics to watch + +- og-route cache hit rate (should be high — every shared, edited board should already have an image). This is now an aggregate only: with no `index1` on these events there is no per-board breakdown and no "first fetch per board". +- `not_rendered_yet` rate (should fall to near-zero for edited boards; every one of these is a crawler sent to the default image rather than the board) +- renders/day by `reason`, and total Browser Run minutes — the real spend signal, since the only bound is per-board and nothing caps the total +- ratio of renders to `persist_success` events: the debounce's whole claim is that this stays well below 1, and it is the number that would show the max wait being hit more often than expected. Note this is now **two aggregate counts divided by each other**, not a per-board join — the screenshot events carry no index. A handful of pathological boards and a uniformly high ratio look the same from here +- queue depth, especially through the first days after deploy, when every actively edited board renders for the first time + +### Open questions + +**These gate the deploy.** The sizing rests on two numbers, and the plausible range spans the account limit, so they want answering before this ships rather than after. Both are answered by telemetry on `persist_success` — in particular by no query against the production database. Question 2 is now answered from production data (see "What it costs"); question 1 still needs this branch's `blob3` deployed, and matters less than it did, since rendering no longer depends on a board being shared. + +Deliberately not a Postgres question. Postgres knows which files are `shared`, but not which are being _edited_, and the only way to approximate that there is a predicate on `file.updatedAt` — a sequential scan of a hot table to answer a capacity-planning question. `persist_success` fires on exactly the event that triggers a render, so it can carry both facts itself. + +1. **What fraction of actively edited boards are link-shared (`f`)?** This is the multiplier on every render estimate here; the current constants assume `f ≈ 30%`. `sharedState` (`blob3`) records it at persist time, from the same `_fileRecordCache.shared` the render trigger gates on, so it is persist-weighted — which is the weighting render cost actually has, unlike a per-file count. + + ```sql + -- f = shared / (shared + private). Check the other rows before trusting it: `legacy` (non-app + -- rooms) and `deleted` never render and are correctly outside the ratio; a large `unknown` means + -- file records often aren't loaded at persist time and the ratio stands on a thin denominator. + SELECT blob3 AS shared_state, SUM(_sample_interval) AS persists + FROM MEASURE + WHERE blob1 = 'persist_success' + AND blob2 = 'production-tldraw-multiplayer' + AND timestamp > NOW() - INTERVAL '1' HOUR + GROUP BY shared_state + ``` + + For the board-weighted figure instead — distinct shared boards rather than persists — group by `index1` with `blob3 = 'shared'` and count the returned rows. + + ```sql + -- Persists per board, keyed on the durable object id (one-way, so it identifies a board without + -- being usable as a board URL). No uniq()/count(distinct) exists in Analytics Engine, so GROUP BY + -- and count the rows. This is also the input to question 2. + SELECT index1 AS durable_object_id, SUM(_sample_interval) AS persists + FROM MEASURE + WHERE blob1 = 'persist_success' + AND blob2 = 'production-tldraw-multiplayer' + AND blob3 = 'shared' + AND timestamp > NOW() - INTERVAL '1' HOUR + GROUP BY durable_object_id + ``` + +2. **How many editing sessions start per minute? — answered.** [PR 9708](https://github.com/tldraw/tldraw/pull/9708) shipped `index1` on file DO events to production, so this no longer waits on deploying this branch. Replaying the debounce over a 30 minute production window gives **~37–51 renders/min** (the spread is Analytics Engine sampling, which splits runs that were really continuous). See "What it costs" for the method, the resulting $250–350/month, and the two consequences: production would sit at 70–95% of Browser Run's 60/min account limit on day one, and `OG_RENDER_MAX_WAIT_MS` is not the cost dial — it fires for under a tenth of renders, because the median editing session spans about ten seconds. + + The query is raw rows rather than an aggregate, since sessions are a property of the sequence: + + ```sql + -- Then group by index1 in the client, sort each board's timestamps, and count runs separated by + -- more than OG_RENDER_DEBOUNCE_MS, adding floor(run length / OG_RENDER_MAX_WAIT_MS) per run. + -- Divide each board's gaps by its own _sample_interval for the low end of the range. + SELECT index1, timestamp, _sample_interval + FROM MEASURE + WHERE blob1 = 'persist_success' + AND blob2 = 'production-tldraw-multiplayer' + AND timestamp > NOW() - INTERVAL '30' MINUTE + LIMIT 100000 + ``` + + Worth re-running before the deploy rather than trusting the number here: it is one window on one afternoon, and it includes `legacy` and `deleted` rooms that never render, which this branch's `blob3` would net out. + +3. Browser Run's account limits (120 concurrent browsers, 1 new browser/second) are the real ceiling now that nothing in this worker caps renders. New-browsers-per-minute is the binding one, not concurrency: even at 217 renders/min with ~8s renders that is only ~29 concurrent against a limit of 120. Worth confirming whether `quickAction` bills against those or against the separate REST quota, and watching the Browser Run dashboard's Runs tab through rollout — the failure mode is a render error and three retries (so, amplification rather than backpressure), not a clean 429. diff --git a/apps/dotcom/sync-worker/src/TLFileDurableObject.ts b/apps/dotcom/sync-worker/src/TLFileDurableObject.ts new file mode 100644 index 0000000..0028790 --- /dev/null +++ b/apps/dotcom/sync-worker/src/TLFileDurableObject.ts @@ -0,0 +1,3531 @@ +/// +/// + +import { SupabaseClient } from '@supabase/supabase-js' +import { + DB, + FILE_PREFIX, + LOCAL_FILE_PREFIX, + PUBLISH_PREFIX, + READ_ONLY_LEGACY_PREFIX, + READ_ONLY_PREFIX, + ROOM_OPEN_MODE, + ROOM_PREFIX, + ROOM_SIZE_LIMIT_MB, + SNAPSHOT_PREFIX, + TLCustomServerEvent, + TlaFile, + WELCOME_CREATE_SOURCE, + can, + type RoomOpenMode, +} from '@tldraw/dotcom-shared' +import { + DEFAULT_INITIAL_SNAPSHOT, + DurableObjectSqliteSyncWrapper, + RoomSnapshot, + SQLiteSyncStorage, + TLSocketRoom, + TLSyncErrorCloseEventCode, + TLSyncErrorCloseEventReason, + TLSyncForwardDiff, + TLSyncStorage, + loadSnapshotIntoStorage, + type PersistedRoomSnapshotForSupabase, + type SessionStateSnapshot, + type TLObjectStoreAccess, +} from '@tldraw/sync-core' +import { + TLAsset, + TLAssetId, + TLComment, + TLCommentReaction, + TLDOCUMENT_ID, + TLDocument, + TLRecord, + commentSchemaRecords, + createTLSchema, + isCommentId, + isCommentReactionId, + isCommentThreadId, +} from '@tldraw/tlschema' +import { ExecutionQueue, assert, assertExists, exhaustiveSwitchError, retry } from '@tldraw/utils' +import { createSentry, isValidR2ObjectName } from '@tldraw/worker-shared' +import { DurableObject } from 'cloudflare:workers' +import { IRequest, Router, StatusError } from 'itty-router' +import { Kysely, PostgresDialect } from 'kysely' +import PQueue from 'p-queue' +import { collectAssetAssociationChanges } from './assetAssociation' +import { SessionMeta, authorizeFileRecord } from './authorizeFileRecord' +import { + CommentLoadResult, + findEmptiedCommentThreads, + findOrphanedReactions, + isCommentAuthorFkViolation, + isCommentFileFkViolation, + isCommentMentionFkViolation, + isCommentReactionFkViolation, + isCommentThreadFkViolation, + isCommentThreadIdFkViolation, + loadCommentDocuments, + mergeCommentDocumentsIntoSnapshot, + outboxEntriesToClear, + planCommentDrain, + planMentionReconciles, + upsertCommentReactionRows, + upsertCommentRows, + upsertCommentThreadRows, +} from './commentRows' +import { MAX_VERIFY_KEYFRAME_BYTES, PERSIST_INTERVAL_MS } from './config' +import { Logger } from './Logger' +import { + ensureMcpClusterIndexTable, + pruneMcpClusterIndexRows, + readMcpClusterIndexRow, + writeMcpClusterIndexRow, +} from './mcpClusterIndexStorage' +import { TLPostgresPool } from './postgres' +import { deleteAllObjectsWithPrefix, getR2KeyForRoom, R2ReadScheduler } from './r2' +import { + BootStage, + FileEffectStallError, + RoomNotFoundError, + SourcePersistTimeoutError, + settleWithin, + shouldSkipMissingRoomEffect, +} from './roomEffectHelpers' +import { getPublishedRoomSnapshot } from './routes/tla/getPublishedFile' +import { deleteBoardThumbnails, enqueueOgImageRender } from './routes/tla/ogImageQueue' +import { + generateSnapshotChunks, + getSnapshotFingerprint, + getSnapshotMetadata, + isSameFingerprint, + resolvePersistedFingerprint, + SnapshotFingerprint, +} from './snapshotUtils' +import { Analytics, DBLoadResult, Environment, McpClusterIndexKey, TLServerEvent } from './types' +import { EventData, writeDataPoint } from './utils/analytics' +import { arrayBufferToBase64 } from './utils/base64' +import { createSupabaseClient } from './utils/createSupabaseClient' +import { getRoomDurableObject } from './utils/durableObjects' +import { LOAD_ID_PARAM, parseLoadId } from './utils/loadId' +import { OgRenderDebouncer } from './utils/ogRenderDebounce' +import { isRateLimited } from './utils/rateLimit' +import { getSlug } from './utils/roomOpenMode' +import { throttle } from './utils/throttle' +import { getAuth, requireAdminAccess, requireAdminAccessToRequest } from './utils/tla/getAuth' +import { getLegacyRoomData } from './utils/tla/getLegacyRoomData' +import { getRole } from './utils/tla/getRole' +import { isTestFile } from './utils/tla/isTestFile' +import { ChainState, isChainHead, PendingDelta } from './versionChain' +import { + loadVersionChainRollout, + resolveVersionChainMode, + VersionChainRollout, +} from './versionChainConfig' +import { + deleteAllVersions, + loadChainIndex, + openWholeVersionStream, + reconstructVersion, +} from './versionChainRead' +import { readOpenSegment, writeVersionChainEntry } from './versionChainWrite' +import { chainHeadHash } from './versionDelta' +import { resolveWelcomeSnapshot } from './welcome/resolveWelcomeSnapshot' + +const MAX_CONNECTIONS = 50 + +// Cloudflare allows at most six simultaneous open connections. We funnel every R2 operation the +// durable object makes — asset copies during association passes AND snapshot uploads during +// persistence — through a single queue so they can never collectively exceed that budget. An +// asset copy holds two connections (the R2 get body streaming into the put); a snapshot upload +// holds ~one at a time (multipart parts are uploaded sequentially). With two operations in flight +// the worst case is two copies = four connections, leaving two free for Postgres +// queries. Without a shared budget the upload and a concurrent association pass contend for the +// same connections, which surfaces as "Network connection lost" during multipart uploads. +// https://developers.cloudflare.com/workers/platform/limits/#simultaneous-open-connections +const MAX_CONCURRENT_R2_OPERATIONS = 2 + +// The shared R2 queue normally sits near empty (two operations in flight, nothing waiting). We only +// emit a depth metric once it backs up past this many operations, to keep the common case out of +// analytics — Grafana can then graph how deep the queue gets, in total and broken down by operation +// type. +const R2_QUEUE_DEPTH_METRIC_THRESHOLD = 30 +// If the queue ever backs up this far, operations are arriving far faster than the two-at-a-time +// budget can drain them and a pass is likely to outlast the durable object. Surface it to Sentry +// (once per sustained spike) so it can alert. +const R2_QUEUE_DEPTH_ALERT_THRESHOLD = 100 + +// The kinds of R2 operation that share the connection budget, used to break queue depth down per +// type in metrics. +type R2OperationType = + | 'asset_copy' + | 'snapshot_upload' + | 'version_chain_write' + | 'version_chain_read' + | 'version_chain_verify' + | 'version_chain_delete' + +// Transient R2 failures worth retrying — dropped connections and the connection-limit error the +// shared budget exists to avoid. Anything else (a bad request, missing object, etc.) is permanent, +// so retrying only wastes time before the simple-PUT fallback runs. +function isTransientConnectionError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error) + return /network|connection|closed|reset|timeout/i.test(message) +} + +// Where the chain state lives in durable object storage; see getVersionChain. +const VERSION_CHAIN_STORAGE_KEY = 'versionChain' + +// increment this any time you make a change to this type +const CURRENT_DOCUMENT_INFO_VERSION = 3 +interface DocumentInfo { + version: number + slug: string + isApp: boolean + deleted: boolean +} + +// Marks "query ran, row absent" inside getAppFileRecord's retry so it can be told apart +// from infra failures, which must throw rather than masquerade as a missing file. +class FileRecordNotFoundError extends Error { + constructor() { + super('File not found') + this.name = 'FileRecordNotFoundError' + } +} + +interface SocketAttachment { + sessionId: string + meta: SessionMeta + isReadonly: boolean + // optional: attachments serialized before the object-store lane existed lack this field + objectAccess?: TLObjectStoreAccess + // The client bundle's monotonic build timestamp (epoch ms), from the `?v=` connect param. + // Absent for bundles that predate the param, or when the param didn't validate. + clientBuildTimestamp?: string + snapshot: SessionStateSnapshot | null +} + +async function canAccessTestProductionFile( + env: Environment, + auth: { userId: string } | null +): Promise { + try { + await requireAdminAccess(env, auth) + return true + } catch (_e) { + return false + } +} + +function pruneUnusedAssetsForTldr(records: TLRecord[]): TLRecord[] { + const usedAssets = new Set() + for (const record of records) { + if (record.typeName === 'shape' && 'assetId' in record.props && record.props.assetId) { + usedAssets.add(record.props.assetId as TLAssetId) + } + } + return records.filter((r) => r.typeName !== 'asset' || usedAssets.has(r.id as TLAssetId)) +} + +const MB = 1024 * 1024 + +// The schema for a file room. Includes the opt-in `comment` record type so comment records sync +// through the room; comments are persisted to Postgres rather than the main document blob (see +// drainCommentOutbox / loadCommentsFromPostgres). The same records must be registered on the +// client (see useSync in the dotcom client) or the schemas won't match. +const fileSyncSchema = createTLSchema({ records: commentSchemaRecords }) + +// Record types served through the room's object-store lane rather than the document lane. +// Object-lane records sync over the same socket but are gated per session by `objectAccess` +// (instead of `isReadonly`), are excluded from `.tldr` downloads, and are persisted in Postgres +// rather than the R2 document blob. Every entry must have an `authorizeFileRecord` authorizer — +// that's where per-tier write policy lives (the `satisfies` enforces it). +const OBJECT_TYPES = [ + 'comment-thread', + 'comment', + 'comment-reaction', +] as const satisfies readonly (keyof typeof authorizeFileRecord)[] + +// The comment outbox (see drainCommentOutbox) only understands comment record ids; keep this +// separate from OBJECT_TYPES so a future object-lane type doesn't get enqueued there. +function isCommentRecordId(id: string) { + return isCommentId(id) || isCommentThreadId(id) || isCommentReactionId(id) +} + +export class TLFileDurableObject extends DurableObject { + // A unique identifier for this instance of the Durable Object + id: DurableObjectId + + private _storage: Promise> | null = null + + private async loadStorage(slug: string): Promise> { + const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage) + + // If SQLite has been initialized, use it directly + if (SQLiteSyncStorage.hasBeenInitialized(sql)) { + return new SQLiteSyncStorage({ sql, objectTypes: OBJECT_TYPES }) + } + + // SQLite not initialized yet, load from R2 and initialize. The loaded snapshot has the + // Postgres comment rows merged in; the storage routes those records into its objects + // partition. + const result = await this.loadFromDatabase(slug) + // Decoding a large board into SQLite is sync CPU; without this it runs under whichever + // network stage loadFromDatabase last set. + this.setBootStage('storage-load:sqlite-init') + const storage = new SQLiteSyncStorage({ + sql, + snapshot: result.snapshot, + objectTypes: OBJECT_TYPES, + }) + // We should not await on setRoomStorageUsedPercentage because it calls + // getStorage under the hood which will only resolve once this function has returned. + this.setRoomStorageUsedPercentage(result.roomSizeMB) + return storage + } + + private getStorage(): Promise> { + if (!this._documentInfo) { + throw new Error('documentInfo must be present when accessing room') + } + if (!this._storage) { + this.setBootStage('storage-load:sqlite-init') + // Kicked off here so the KV read resolves alongside the room load instead of after it. + this.versionChainRollout() + const promise = retry(() => this.loadStorage(this.documentInfo.slug), { + // Allow RoomNotFoundError to bubble up since it means the room doesn't exist + // and there's no point in retrying. + matchError: (error) => !(error instanceof RoomNotFoundError), + }) + .then(async (storage) => { + storage.onChange(() => { + this.triggerPersist() + }) + storage.transaction((txn) => { + fileSyncSchema.migrateStorage(txn) + }) + this.setBootStage('storage-load:kv-rollout') + // The next persist diffs against this rather than cutting a keyframe every time the + // durable object wakes. It is usually what R2 holds, but not always: a previous + // incarnation can die with edits SQLite has and R2 does not. That is safe because the + // seed is never trusted as the chain head — decideVersionWrite checks it against the + // head the chain recorded and cuts a keyframe when they differ. Gated on the mode: this + // is a second decoded copy of the board pinned for the DO's lifetime, not worth paying + // for where chains are off. + const rollout = await this.versionChainRollout() + if (resolveVersionChainMode(rollout, getR2KeyForRoom(this.documentInfo)) !== 'off') { + this._lastPersistedSnapshot = storage.getSnapshot?.() ?? null + } + // Drain any outbox entries stranded by a previous incarnation (e.g. a Postgres + // blip during the last-out drain). Retries are otherwise onChange-driven + // (triggerPersist), so a reopened room where users only view would never drain. + // Deferred to a microtask because drainCommentOutbox's queued task awaits + // getStorage() — i.e. the very promise this callback resolves. Even a + // synchronous call here wouldn't deadlock (ExecutionQueue starts an idle + // queue's task synchronously, but the task only suspends on the + // already-assigned `_storage` promise — awaiting never blocks); kicking after + // this callback returns just keeps the non-reentrancy obvious. An empty + // outbox no-ops after a couple of synchronous SQL statements, so the cost per + // room start is trivial. + queueMicrotask(() => void this.drainCommentOutbox()) + // Clear here, not only when the room settles: storage-only callers (restore, + // .tldr download) never boot the room and would leave a stale stage behind. + this.setBootStage(null) + return storage + }) + .catch((error) => { + // RoomNotFoundError is an expected outcome (missing room), not an infra failure; + // callers decide how to surface it, so don't Sentry-report it here. + if (!(error instanceof RoomNotFoundError)) this.reportError(error) + // Never cache a rejection: the condition may heal, and a cached rejection + // makes every later retry fail instantly. + if (this._storage === promise) this._storage = null + this.setBootStage(null) + throw error + }) + this._storage = promise + } + return this._storage + } + + _room: Promise> | null = null + + // Which boot phase a pending _storage/_room promise last reached, and when it was entered. + // Pending promises are cached, so when a boot wedges every caller awaits the same stuck + // await — this names it and its age (see FileEffectStallError and __admin__getDocumentInfo). + _bootStage: { stage: BootStage; startedAt: number } | null = null + + private setBootStage(stage: BootStage | null) { + this._bootStage = stage === null ? null : { stage, startedAt: Date.now() } + } + + sentry: ReturnType | null = null + + getRoom() { + if (!this._documentInfo) { + throw new Error('documentInfo must be present when accessing room') + } + if (!this._room) { + const promise = this.getStorage() + .then(async (storage) => { + this.setBootStage('room-create') + const room = new TLSocketRoom({ + storage, + schema: fileSyncSchema, + objectTypes: OBJECT_TYPES, + clientTimeout: Infinity, + log: { + warn: (...args) => this.log.debug('sync warn', ...args), + error: (...args) => { + this.reportError(args.find((a) => a instanceof Error) ?? new Error(args.join(' '))) + }, + }, + onSessionSnapshot: (sessionId, snapshot) => { + const ws = this.sessionIdToWs.get(sessionId) + if (!ws) return + const attachment = this.getSocketAttachment(ws) + if (!attachment) return + ws.serializeAttachment({ ...attachment, snapshot }) + }, + onSessionRemoved: async (room, args) => { + this._pendingFirstLoadEchoes.delete(args.sessionId) + this.logEvent({ + type: 'client', + name: 'leave', + instanceId: args.sessionId, + }) + + if (args.numSessionsRemaining > 0) return + if (!this._room) return + this.logEvent({ + type: 'client', + name: 'last_out', + instanceId: args.sessionId, + }) + try { + await this.persistToDatabase() + } catch { + // already logged + } + // make sure nobody joined the room while we were persisting + if (room.getNumActiveSessions() > 0) return + this._room = null + this.dropBootTimings() + room.close() + this.logEvent({ type: 'room', name: 'room_empty' }) + await this._pool?.end() + this._pool = null + this._db = null + }, + onBeforeSendMessage: ({ sessionId, message, stringified }) => { + this.logEvent({ + type: 'send_message', + messageType: message.type, + messageLength: stringified.length, + }) + if (message.type === 'connect') { + const echo = this._pendingFirstLoadEchoes.get(sessionId) + if (echo) { + this._pendingFirstLoadEchoes.delete(sessionId) + // Deferred: this hook runs before the connect response goes out, and sending + // here would put the echo ahead of it on the wire. + setTimeout(() => room.sendCustomMessage(sessionId, echo), 0) + } + } + }, + // Record object-lane (comment) changes in the durable outbox as soon as they + // commit and push them to Postgres (not on the throttled R2 persist) so Zero + // replicates them to the app-level view quickly. + onCommittedChanges: ({ diff }) => { + this.enqueueCommentChanges(diff) + }, + // Guard attribution with the session's authenticated identity so a client can't + // post or edit in someone else's name. + authorizeRecord: authorizeFileRecord, + }) + + // Sessions that survived hibernation. Collected before the event so room_start can + // carry the count, and resumed after it so a resume that throws still leaves the + // boot counted — those are the boots the count exists to find. + const resumes: { + sessionId: string + socket: WebSocket + snapshot: SessionStateSnapshot + meta: SessionMeta + }[] = [] + for (const ws of this.state.getWebSockets()) { + const attachment = ws.deserializeAttachment() as SocketAttachment | null + if (!attachment?.sessionId) continue + if (attachment.snapshot) { + resumes.push({ + sessionId: attachment.sessionId, + socket: ws, + snapshot: attachment.snapshot, + meta: attachment.meta, + }) + } + } + + this.logEvent({ type: 'room', name: 'room_start', resumedSockets: resumes.length }) + + for (const resume of resumes) { + room.handleSocketResume(resume) + } + // Also associate file assets after we load the room + setTimeout(this.maybeAssociateFileAssets.bind(this), PERSIST_INTERVAL_MS) + this.setBootStage(null) + return room + }) + .catch((error) => { + // Never cache a rejection: the condition may heal, and a cached rejection + // makes every later retry fail instantly. + if (this._room === promise) { + this._room = null + this.dropBootTimings() + } + this.setBootStage(null) + throw error + }) + this._room = promise + } + return this._room + } + + // For storage + storage: DurableObjectStorage + + // For persistence + private _supabaseClient: SupabaseClient | undefined + + // For analytics + measure: Analytics | undefined + readonly supabaseTable: string + readonly r2: { + readonly rooms: R2Bucket + readonly versionCache: R2Bucket + readonly versionChain: R2Bucket + } + + _documentInfo: DocumentInfo | null = null + + _db: Kysely | null = null + _pool: TLPostgresPool | null = null + private readonly log: Logger + /** Map sessionId → ws so onSessionSnapshot can serialize to the right socket. */ + private readonly sessionIdToWs = new Map() + + /** + * Only legacy (non-app) rooms ever read from supabase, so don't construct the client until one + * asks for it — most DOs never will. + */ + // eslint-disable-next-line tldraw/no-setter-getter + get supabaseClient() { + if (!this._supabaseClient) { + this._supabaseClient = createSupabaseClient(this.env) + } + return this._supabaseClient + } + + // eslint-disable-next-line tldraw/no-setter-getter + get db() { + if (!this._db) { + this._pool = new TLPostgresPool(this.env, this.log) + this._db = new Kysely({ + dialect: new PostgresDialect({ pool: this._pool }), + log: ['error'], + }) + } + return this._db + } + + constructor( + private state: DurableObjectState, + override env: Environment + ) { + super(state, env) + this.id = state.id + this.storage = state.storage + this.measure = env.MEASURE + this.sentry = createSentry(this.state, this.env) + this.log = new Logger(env, 'TLDrawDurableObject', this.sentry) + + this.supabaseTable = env.TLDRAW_ENV === 'production' ? 'drawings' : 'drawings_staging' + this.r2 = { + rooms: env.ROOMS, + versionCache: env.ROOMS_HISTORY_EPHEMERAL, + versionChain: env.ROOMS_HISTORY, + } + + // Respond to ping at the platform layer so the DO can hibernate + this.state.setWebSocketAutoResponse( + new WebSocketRequestResponsePair('{"type":"ping"}', '{"type":"pong"}') + ) + + // Nothing awaits this, so a storage failure here rejects into the void and takes the object + // down with it — the one path that only runs on a cold start, and so never ran at all while + // the file DO couldn't hibernate. Rethrown after reporting: the object genuinely cannot + // continue without documentInfo, and resetting it is the runtime's correct response. + state.blockConcurrencyWhile(async () => { + try { + const existingDocumentInfo = (await this.storage.get('documentInfo')) as DocumentInfo | null + if (existingDocumentInfo?.version !== CURRENT_DOCUMENT_INFO_VERSION) { + this._documentInfo = null + } else { + this._documentInfo = existingDocumentInfo + } + } catch (e) { + this.reportError(e, { source: 'blockConcurrencyWhile' }) + throw e + } + }) + } + + readonly router = Router() + .get( + `/${ROOM_PREFIX}/:roomId`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE), + (req) => this.onRequest(req, ROOM_OPEN_MODE.READ_WRITE) + ) + .get( + `/${READ_ONLY_LEGACY_PREFIX}/:roomId`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_ONLY_LEGACY), + (req) => this.onRequest(req, ROOM_OPEN_MODE.READ_ONLY_LEGACY) + ) + .get( + `/${READ_ONLY_PREFIX}/:roomId`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_ONLY), + (req) => this.onRequest(req, ROOM_OPEN_MODE.READ_ONLY) + ) + .get( + `/app/file/:roomId`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE), + (req) => this.onRequest(req, ROOM_OPEN_MODE.READ_WRITE) + ) + .get( + `/app/file/:roomId/download`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE), + (req) => this.onDownloadTldr(req) + ) + .post( + `/${ROOM_PREFIX}/:roomId/restore`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE), + (req) => this.onRestore(req) + ) + .post( + `/app/file/:roomId/restore`, + (req) => this.extractDocumentInfoFromRequest(req, ROOM_OPEN_MODE.READ_WRITE), + (req) => this.onRestore(req) + ) + .all('*', () => new Response('Not found', { status: 404 })) + + // eslint-disable-next-line tldraw/no-setter-getter + get documentInfo() { + return assertExists(this._documentInfo, 'documentInfo must be present') + } + setDocumentInfo(info: Omit) { + this._documentInfo = { version: CURRENT_DOCUMENT_INFO_VERSION, ...info } + this.storage.put('documentInfo', this._documentInfo) + } + async extractDocumentInfoFromRequest(req: IRequest, roomOpenMode: RoomOpenMode) { + const slug = assertExists( + await getSlug(this.env, req.params.roomId, roomOpenMode), + 'roomId must be present' + ) + const isApp = new URL(req.url).pathname.startsWith('/app/') + + if (this._documentInfo) { + assert(this._documentInfo.slug === slug, 'slug must match') + } else { + this.setDocumentInfo({ slug, isApp, deleted: false }) + } + } + + // Handle a request to the Durable Object. + override async fetch(req: IRequest) { + const sentry = createSentry(this.state, this.env, req) + + try { + return await this.router.fetch(req) + } catch (err) { + // Auth failures (e.g. non-staff hitting restore) are expected denials, not + // server errors: return their real status instead of a 500 + Sentry noise. + if (err instanceof StatusError) { + return new Response(err.message, { status: err.status }) + } + console.error(err) + // eslint-disable-next-line @typescript-eslint/no-deprecated + sentry?.captureException(err) + return new Response('Something went wrong', { + status: 500, + statusText: 'Internal Server Error', + }) + } + } + + // --- WebSocket hibernation API handlers --- + + private getSocketAttachment(ws: WebSocket): SocketAttachment | null { + return ws.deserializeAttachment() as SocketAttachment | null + } + + // The runtime discards whatever these handlers reject with, so anything that escapes them is + // invisible: getRoom() in particular can fail while loading the room from storage, which breaks + // the main sync path. Catch and report instead. + override async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { + try { + const attachment = this.getSocketAttachment(ws) + if (!attachment?.sessionId) return + if (!this._documentInfo) return + + this.sessionIdToWs.set(attachment.sessionId, ws) + const room = await this.getRoom() + room.handleSocketMessage(attachment.sessionId, message) + } catch (e) { + if (e instanceof RoomNotFoundError) { + // Post-hibernation resume raced a deleted room; there's no room left to message. + ws.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.NOT_FOUND) + return + } + // Report before closing, so a throw from close() can't cost us the Sentry event. + this.reportError(e, { source: 'webSocketMessage' }) + // The message is lost either way, so the socket has to go: leaving it up would let the + // client carry on believing it is synced while its local state silently diverges. Closed + // without a code, though — TLSyncErrorCloseEventCode is fatal on the client, which tears + // the sync client down into an error state only a reload clears. What lands here is + // transient (a getRoom() that failed resuming from hibernation), so a codeless close + // reads as 'offline' and the reconnect manager reconnects and resyncs from scratch. + ws.close() + } + } + + override async webSocketClose(ws: WebSocket) { + await this.handleWebSocketEnd(ws, 'handleSocketClose') + } + + override async webSocketError(ws: WebSocket, error: unknown) { + // The socket failed rather than closing cleanly. That error was previously dropped on the + // floor, so a client whose connection kept breaking left no trace here at all. + this.reportError(error, { source: 'webSocketError' }) + await this.handleWebSocketEnd(ws, 'handleSocketError') + } + + private async handleWebSocketEnd( + ws: WebSocket, + method: 'handleSocketClose' | 'handleSocketError' + ) { + try { + const attachment = this.getSocketAttachment(ws) + if (!attachment?.sessionId) return + + this.sessionIdToWs.delete(attachment.sessionId) + if (!this._documentInfo) return + // A deleted room has nothing left to broadcast, and booting one here would resume the + // just-closed sessions onto storage the delete is wiping. + if (this._documentInfo.deleted) return + + const room = await this.getRoom() + + // If the DO was hibernating, this session was never re-added to the room. + // Resume it briefly so the room can broadcast presence removal to other clients. + if (attachment.snapshot && !room.getSessionSnapshot(attachment.sessionId)) { + room.handleSocketResume({ + sessionId: attachment.sessionId, + socket: ws, + snapshot: attachment.snapshot, + meta: attachment.meta, + }) + } + + room[method](attachment.sessionId) + } catch (e) { + if (e instanceof RoomNotFoundError) { + // The socket is already closing/closed; nothing left to clean up on the room side. + console.error('handleWebSocketEnd: room not found, skipping', e) + return + } + // Both callers await this, so a failure here would otherwise reject their handler and + // vanish. Reported rather than rethrown: the socket is already gone either way, and the + // only thing lost is presence cleanup for other clients in the room. + this.reportError(e, { source: method }) + } + } + + // The snapshot the last persist wrote, so the next one can diff against it. Seeded on wake from + // the document just loaded so a cold start does not cut a keyframe; see getStorage for why that + // seed may be ahead of R2 and why that is safe. + _lastPersistedSnapshot: RoomSnapshot | null = null + // `chainHeadHash(_lastPersistedSnapshot)`, kept from the write that produced it so the next + // persist does not hash the whole board again just to check the diff base. Null when unknown + // (the wake seed); the write computes it once and then it is known. + _lastPersistedHeadHash: string | null = null + _versionChain: ChainState | null = null + _versionChainLoaded = false + // The open segment's deltas. Null means "not known here yet" — after an eviction they are + // refetched from the segment object in R2, which is the durable copy. + _pendingDeltas: PendingDelta[] | null = null + // The version key the chain head was written under, so a retried persist can put the legacy + // copy of the same content under the same key. + _versionChainHeadIso: string | null = null + + _versionChainRollout: Promise | null = null + + // One KV read per incarnation, by design: the rollout is config, not room state, and a KV flip + // landing as objects wake is the contract (see loadVersionChainRollout). + private versionChainRollout(): Promise { + this._versionChainRollout ??= loadVersionChainRollout(this.env) + return this._versionChainRollout + } + + private async getVersionChain(): Promise { + if (!this._versionChainLoaded) { + this._versionChain = + ((await this.storage.get(VERSION_CHAIN_STORAGE_KEY)) as ChainState | null) ?? null + this._versionChainLoaded = true + } + return this._versionChain + } + + private async getPendingDeltas(chain: ChainState | null): Promise { + if (!chain?.openSegment) return [] + if (this._pendingDeltas) return this._pendingDeltas + // Cold start. Appending means rewriting the whole segment, so the deltas already in it have + // to come back — reading the object is cheaper and simpler than keeping a second copy of + // them in durable object storage, and it cannot drift from what R2 actually holds. + // Null, not []: an open segment that vanished or cannot be decoded is a broken chain, and + // rewriting it from an empty buffer would silently erase the deltas its metadata still + // promises. The caller starts a fresh chain on null. + const segmentKey = chain.openSegment.key + const deltas = await this.addR2Operation('version_chain_write', () => + retry(() => readOpenSegment(this.r2.versionChain, segmentKey), { + attempts: 3, + waitDuration: 500, + matchError: isTransientConnectionError, + }) + ) + if (deltas) this._pendingDeltas = deltas + return deltas + } + + _isRestoring = false + async onRestore(req: IRequest) { + this._isRestoring = true + try { + await requireAdminAccessToRequest(req, this.env) + const roomId = this.documentInfo.slug + const roomKey = getR2KeyForRoom({ slug: roomId, isApp: this.documentInfo.isApp }) + const timestamp = ((await req.json()) as any).timestamp + if (!timestamp) { + return new Response('Missing timestamp', { status: 400 }) + } + // Reconstructs from the chain, falling back to the legacy full copy both when the chain + // has nothing for this version and when it is broken — an admin who can preview a version + // must be able to restore it while the full copies exist. + // Whole objects (keyframes, legacy copies) are read as text once — the same cost as the + // handler this replaces — and only a delta replay materializes a snapshot, which is then + // reused below rather than re-parsed. Parsing, re-serializing and parsing again a large + // board is what pushes a 128MB isolate over. + let dataText: string + let restored: RoomSnapshot | undefined + try { + // Each read is its own queued operation rather than the whole restore holding one + // slot — a slot is sized for an asset copy's two connections, and reconstruction + // fans out to the keyframe plus every segment (see _verifyRetiredChain). Every read + // is one idempotent get or list, so each retries transient errors on its own. + const schedule: R2ReadScheduler = (read) => + this.addR2Operation('version_chain_read', () => + retry(read, { attempts: 3, waitDuration: 500, matchError: isTransientConnectionError }) + ) + const buckets = { + chainBucket: this.r2.versionChain, + legacyBucket: this.r2.versionCache, + } + const { entries: index } = await loadChainIndex(this.r2.versionChain, roomKey, schedule) + const whole = await openWholeVersionStream({ + ...buckets, + roomKey, + timestamp, + index, + schedule, + }) + if (whole) { + dataText = await new Response(whole).text() + } else { + const reconstruction = await reconstructVersion({ + ...buckets, + roomKey, + timestamp, + index, + schedule, + }) + if (!reconstruction) { + return new Response('Version not found', { status: 400 }) + } + restored = reconstruction.snapshot + dataText = JSON.stringify(restored) + } + } catch (error) { + const legacy = await this.r2.versionCache.get(`${roomKey}/${timestamp}`) + if (!legacy) throw error + this.reportError(error) + dataText = await legacy.text() + } + + // The put deliberately carries no version metadata, so null ("looked up, no usable + // stamp") is the truth about R2 and the next persist re-uploads and re-stamps. + // Queued on executionQueue because a persist runs as one task there: an in-flight one, + // suspended mid-upload or at its version lookup, would otherwise resume after these + // lines and clobber both the null and the restored object. + await this.executionQueue.push(async () => { + await this.r2.rooms.put(roomKey, dataText) + this._lastPersistedFingerprint = null + // The chain head no longer matches the rooms object. The fingerprint check would + // catch the stale chain on the next persist anyway; clearing here makes the next + // version an intentional keyframe rather than a recovered mistake. + this._lastPersistedSnapshot = null + this._lastPersistedHeadHash = null + this._versionChain = null + this._versionChainLoaded = true + this._pendingDeltas = null + this._versionChainHeadIso = null + await this.storage.delete(VERSION_CHAIN_STORAGE_KEY) + }) + + // Version snapshots only contain the drawing data. Restoring drops the file's comments + // (product decision): loading the bare snapshot wipes the object lane, and the Postgres + // rows are deleted explicitly since storage transactions don't fire onCommittedChanges. + // The cleanup runs through _objectPushQueue so it serializes after any in-flight outbox + // drain, and pre-restore outbox entries are cleared so a pending drain can't resurrect + // deleted rows. + // + // Two invariants make this safe against comments committed concurrently with the + // restore: + // (a) The lane wipe, the outbox high-water-mark capture, and the queue push below run + // in one synchronous block with NO awaits in between. Storage change notifications + // fire on a microtask, so a comment committed after the wipe enqueues its outbox + // entry (seq > maxSeq) and its drain task strictly after ours (queue is FIFO). + // Drains are bounded to the outbox high-water mark captured when they are + // scheduled (see drainCommentOutbox), so no drain queued before ours can touch + // that entry: it survives our scoped clear and its own later drain pushes it to + // Postgres. + // (b) The queue's FIFO order means the cleanup task below runs BEFORE any post-wipe + // comment's drain task, so the fileId-wide Postgres deletes can only remove + // pre-restore rows — never rows a post-wipe drain already wrote. The post-wipe + // entry (seq > maxSeq) survives the scoped outbox clear, and its own drain + // re-upserts the row to Postgres AFTER the cleanup. + // + // Inside the task, the Postgres deletes run BEFORE the outbox clear: if a delete + // throws, any pre-restore outbox entries survive, so subsequent drains retry the + // per-id deletes (the records are lane-absent after the wipe, taking the delete + // path). But a quiescent room has an empty outbox — no per-id retries at all — so + // the only guaranteed retry vehicle is this handler's 500 telling the caller to + // retry the whole restore. Clearing the outbox first would destroy even the per-id + // retries and let the dropped comments resurrect on the next fresh-SQLite load. + // follow-up: a durable wipe-marker recorded alongside the outbox would let the DO + // itself retry the fileId-wide delete, closing the dependence on caller retries. + const snapshot = restored ?? (JSON.parse(dataText) as RoomSnapshot) + + const storage = await this.getStorage() + storage.transaction((txn) => { + loadSnapshotIntoStorage(txn, fileSyncSchema, snapshot) + }) + this.ensureCommentOutbox() + const maxSeq = Number( + this.ctx.storage.sql + .exec('SELECT COALESCE(MAX(seq), 0) AS maxSeq FROM comment_outbox') + .toArray()[0].maxSeq + ) + const cleanup = this._objectPushQueue.push(async () => { + // comment.threadId is NOT NULL with an ON DELETE CASCADE FK, so deleting the + // file's threads provably deletes all its comments too + await this.db.deleteFrom('comment_thread').where('fileId', '=', roomId).execute() + this.ctx.storage.sql.exec('DELETE FROM comment_outbox WHERE seq <= ?', maxSeq) + }) + await cleanup + + this.maybeAssociateFileAssets() + + return new Response() + } finally { + this._isRestoring = false + } + } + + // this might return null if the file doesn't exist yet in the backend, or if it was deleted + _fileRecordCache: TlaFile | null = null + async getAppFileRecord(loadIdBlobs?: string[]): Promise { + const timer = this.timer() + try { + const result = await retry( + async () => { + if (this._fileRecordCache) { + return this._fileRecordCache + } + + const result = await this.db + .selectFrom('file') + .where('id', '=', this.documentInfo.slug) + .selectAll() + .executeTakeFirst() + + if (!result) { + throw new FileRecordNotFoundError() + } + this._fileRecordCache = result + return this._fileRecordCache + }, + // Absence is retried because the row may still be committing. Query errors are retried + // too: isTransientConnectionError is R2-shaped and misses Postgres errors like "too many + // clients already" or ECONNREFUSED, and failing those fast only saves the 2s budget. + { attempts: 20, waitDuration: 100 } + ) + + timer.report('get_file_record', loadIdBlobs) + return result + } catch (e) { + timer.report('get_file_record_error', loadIdBlobs) + if (e instanceof FileRecordNotFoundError) return null + // Query errors are infra failures, not absence: bubble so callers retry + // instead of treating the room as nonexistent. + throw e + } + } + + async onRequest(req: IRequest, openMode: RoomOpenMode) { + const requestTimer = this.timer() + + // extract query params from request, should include instanceId + const url = new URL(req.url) + const params = Object.fromEntries(url.searchParams.entries()) + let { sessionId, storeId } = params + + // handle legacy param names + sessionId ??= params.sessionKey ?? params.instanceId + storeId ??= params.localClientId + // the client bundle's build timestamp. Unvalidated client input that lands in analytics, so + // only accept a plain epoch-ms number — anything else (empty, non-numeric, absurdly long) + // is treated the same as an absent param rather than recorded verbatim. + const clientBuildTimestamp = /^\d{1,16}$/.test(params.v ?? '') ? params.v : undefined + const loadId = parseLoadId(params[LOAD_ID_PARAM]) + const loadIdBlobs = this.loadIdBlobs(loadId) + const isNewSession = !this._room + if (isNewSession) this._bootLoadId = loadId + + // Create the websocket pair for the client; use hibernation API + const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair() + this.state.acceptWebSocket(serverWebSocket) + + const closeSocket = (reason: TLSyncErrorCloseEventReason) => { + serverWebSocket.close(TLSyncErrorCloseEventCode, reason) + return new Response(null, { status: 101, webSocket: clientWebSocket }) + } + // For infra failures (Postgres, rate limiter, etc.): a TLSyncErrorCloseEventCode close is + // terminal on the client (error screen, no reconnect), which would strand every connecting + // user for the length of a transient blip. Any other code puts the client in 'offline' and + // ReconnectManager retries with backoff, matching what the pre-accept 500 → 1006 used to + // do. Workers only allow 1000 or 3000-4999 here. + const closeSocketRetryable = () => { + serverWebSocket.close(1000, 'transient_error') + return new Response(null, { status: 101, webSocket: clientWebSocket }) + } + + // Everything from here through the permission checks below can throw on an infra failure + // now that those failures bubble instead of being swallowed. An uncaught throw here would + // 500 with the accepted server socket leaked in the hibernation set, so catch broadly and + // close it instead. + const echoTimings: { auth?: number; fileRecord?: number } = {} + let auth: Awaited> + try { + if (this.documentInfo.deleted) { + return closeSocket(TLSyncErrorCloseEventReason.NOT_FOUND) + } + + const authTimer = this.timer() + auth = await getAuth(req, this.env) + echoTimings.auth = authTimer.report('on_request_auth', loadIdBlobs) + + if (this.documentInfo.isApp) { + openMode = ROOM_OPEN_MODE.READ_WRITE + const fileRecordStart = Date.now() + const file = await this.getAppFileRecord(loadIdBlobs) + echoTimings.fileRecord = Date.now() - fileRecordStart + + if (file) { + if (file.isDeleted) { + return closeSocket(TLSyncErrorCloseEventReason.NOT_FOUND) + } + + if (isTestFile(file.id) && !(await canAccessTestProductionFile(this.env, auth))) { + return closeSocket(TLSyncErrorCloseEventReason.NOT_FOUND) + } + + if (!auth && !file.shared) { + return closeSocket(TLSyncErrorCloseEventReason.NOT_AUTHENTICATED) + } + + const rateLimitTimer = this.timer() + if (auth?.userId) { + const rateLimited = await isRateLimited(this.env, auth.userId) + if (rateLimited) { + this.logEvent({ + type: 'client', + userId: auth.userId, + name: 'rate_limited', + }) + return closeSocket(TLSyncErrorCloseEventReason.RATE_LIMITED) + } + } else { + const rateLimited = await isRateLimited(this.env, sessionId) + if (rateLimited) { + this.logEvent({ + type: 'client', + userId: auth?.userId, + name: 'rate_limited', + }) + return closeSocket(TLSyncErrorCloseEventReason.RATE_LIMITED) + } + } + rateLimitTimer.report('on_request_rate_limit', loadIdBlobs) + + // Check if user has owner access (directly or via group membership) + let hasOwnerAccess = false + if (file.owningGroupId && auth?.userId) { + // Check the user can access the owning group's files + const groupCheckTimer = this.timer() + const role = await getRole(this.db, auth.userId, file.owningGroupId) + if (can(role, 'accessFiles')) { + hasOwnerAccess = true + } + groupCheckTimer.report('on_request_group_check', loadIdBlobs) + } + + if (!hasOwnerAccess && !file.shared) { + return closeSocket(TLSyncErrorCloseEventReason.FORBIDDEN) + } + + // Guests only get canvas write on an `edit` link. `sharedLinkType` is a plain + // string column with legacy values in it, so anything else fails closed. + if (!hasOwnerAccess && file.sharedLinkType !== 'edit') { + openMode = ROOM_OPEN_MODE.READ_ONLY + } + } else { + // No file row means every check above was skipped, so admitting the socket would + // open the room to anyone holding the id — the R2 blob and the DO's SQLite outlive + // the row through the whole hard-delete window (and forever if the delete effect + // parks). Fail closed, like onDownloadTldr. + return closeSocket(TLSyncErrorCloseEventReason.NOT_FOUND) + } + } else { + // Legacy rooms are now read-only + openMode = ROOM_OPEN_MODE.READ_ONLY + } + } catch (e) { + this.reportError(e) + return closeSocketRetryable() + } + + try { + const meta: SessionMeta = { + storeId: storeId ?? sessionId, + userId: auth?.userId ? auth.userId : null, + } + const isReadonly = openMode === ROOM_OPEN_MODE.READ_ONLY + // Only authenticated users can write comments — authors are stored in Postgres with a + // foreign key to the user table, so an anonymous author can't be represented. Tier + // gating (view-only sessions) happens in `authorizeFileRecord` off the session's + // `isReadonly`. + const objectAccess: TLObjectStoreAccess = auth?.userId ? 'write' : 'read' + const attachment: SocketAttachment = { + sessionId, + meta, + isReadonly, + objectAccess, + clientBuildTimestamp, + snapshot: null, + } + serverWebSocket.serializeAttachment(attachment) + + const getRoomTimer = this.timer() + const room = await this.getRoom() + const getRoomMs = getRoomTimer.report('on_request_get_room', loadIdBlobs) + + // Don't connect if we're already at max connections + if (room.getNumActiveSessions() >= MAX_CONNECTIONS) { + return closeSocket(TLSyncErrorCloseEventReason.ROOM_FULL) + } + + // all good + room.handleSocketConnect({ + sessionId, + socket: serverWebSocket, + meta, + isReadonly, + objectAccess, + }) + if (isNewSession) { + this.logEvent({ + type: 'client', + name: 'room_reopen', + instanceId: sessionId, + }) + } + this.logEvent({ + type: 'client', + name: 'enter', + instanceId: sessionId, + clientBuildTimestamp, + }) + + const totalMs = requestTimer.report('on_request_total', loadIdBlobs) + + if (loadId) { + const boot = isNewSession ? this._bootTimings : {} + // Parked, not sent: the session is still awaiting its connect handshake here, and the + // room drops messages to sessions that are not yet Connected. onBeforeSendMessage + // releases it when the connect response goes out. + this._pendingFirstLoadEchoes.set(sessionId, { + type: 'first_load_server', + loadId, + cold: isNewSession, + auth_ms: echoTimings.auth, + file_record_ms: echoTimings.fileRecord, + get_room_ms: getRoomMs, + total_ms: totalMs, + boot_r2_ms: boot.r2, + boot_comments_ms: boot.comments, + boot_total_ms: boot.total, + }) + } + + return new Response(null, { status: 101, webSocket: clientWebSocket }) + } catch (e) { + if (e instanceof RoomNotFoundError) { + return closeSocket(TLSyncErrorCloseEventReason.NOT_FOUND) + } + this.reportError(e) + return closeSocketRetryable() + } + } + + /** Stream .tldr download (schema + records, R2 assets inlined as base64). Same access as joining the file. */ + async onDownloadTldr(req: IRequest): Promise { + const TLDRAW_FILE_MIMETYPE = 'application/vnd.tldraw+json' + const TLDRAW_FILE_FORMAT_VERSION = 1 + + if (!this.documentInfo.isApp) { + return new Response('Not found', { status: 404 }) + } + + const auth = await getAuth(req, this.env) + const file = await this.getAppFileRecord() + if (!file || file.isDeleted) { + return new Response('Not found', { status: 404 }) + } + + if (isTestFile(file.id) && !(await canAccessTestProductionFile(this.env, auth))) { + return new Response('Not found', { status: 404 }) + } + if (!auth && !file.shared) { + return new Response('Unauthorized', { status: 401 }) + } + + const url = new URL(req.url) + const sessionId = + url.searchParams.get('instanceId') ?? url.searchParams.get('sessionId') ?? 'anon-download' + const rateLimitKey = auth?.userId ?? sessionId + if (await isRateLimited(this.env, rateLimitKey)) { + return new Response('Rate limited', { status: 429 }) + } + + let hasOwnerAccess = false + if (file.owningGroupId && auth?.userId) { + const role = await getRole(this.db, auth.userId, file.owningGroupId) + if (can(role, 'accessFiles')) { + hasOwnerAccess = true + } + } + if (!hasOwnerAccess && !file.shared) { + return new Response('Forbidden', { status: 403 }) + } + + const storage = await this.getStorage() + assert(storage instanceof SQLiteSyncStorage, 'storage must be a SQLiteSyncStorage') + // the document snapshot never contains object-lane records (comments), so .tldr is clean + const snapshot = storage.getSnapshot() + const records = pruneUnusedAssetsForTldr(snapshot.documents.map((d) => d.state) as TLRecord[]) + + const assetRows = await this.db + .selectFrom('asset') + .where('fileId', '=', this.documentInfo.slug) + .select('objectName') + .execute() + const assetObjectNames = new Set(assetRows.map((r) => r.objectName)) + + const documentRecord = records.find((r) => r.typeName === 'document') as TLDocument | undefined + // Prefer the TlaFile.name (kept in sync by the app layer) over the TLDocument.name + // (which is only updated when the document is open in an editor). + const rawName = file.name?.trim() || documentRecord?.name?.trim() + const sanitized = + rawName?.replace(/[^ \w-]/g, '_').slice(0, 200) || `${this.documentInfo.slug}.tldr` + const filename = sanitized.endsWith('.tldr') ? sanitized : `${sanitized}.tldr` + + const env = this.env + const stream = new ReadableStream({ + async start(controller) { + try { + const encoder = new TextEncoder() + controller.enqueue( + encoder.encode( + `{"tldrawFileFormatVersion":${TLDRAW_FILE_FORMAT_VERSION},"schema":${JSON.stringify(snapshot.schema)},"records":[` + ) + ) + for (let i = 0; i < records.length; i++) { + let record = records[i] as TLRecord + const assetSrc = record.typeName === 'asset' ? (record as TLAsset).props.src : null + if ( + record.typeName === 'asset' && + (record as TLAsset).type !== 'bookmark' && + assetSrc && + !assetSrc.startsWith('data:') + ) { + const objectName = new URL(assetSrc).pathname.split('/').pop() + if ( + objectName && + assetObjectNames.has(objectName) && + isValidR2ObjectName(objectName) + ) { + const blob = await env.UPLOADS.get(objectName) + if (blob) { + const ab = await blob.arrayBuffer() + const base64 = arrayBufferToBase64(ab) + const assetRecord = record as TLAsset + const mimeType = + assetRecord.type !== 'bookmark' && + 'mimeType' in assetRecord.props && + assetRecord.props.mimeType + ? assetRecord.props.mimeType + : 'application/octet-stream' + record = { + ...record, + props: { + ...(record as TLAsset).props, + src: `data:${mimeType};base64,${base64}`, + }, + } as TLRecord + } + } + } + controller.enqueue(encoder.encode((i > 0 ? ',' : '') + JSON.stringify(record))) + } + controller.enqueue(encoder.encode(']}')) + controller.close() + } catch (e) { + controller.error(e) + } + }, + }) + + return new Response(stream, { + headers: { + 'Content-Type': TLDRAW_FILE_MIMETYPE, + 'Content-Disposition': `attachment; filename="${filename}"`, + }, + }) + } + + triggerPersist = throttle(() => { + this.persistToDatabase() + }, PERSIST_INTERVAL_MS) + + // Whether a persist on this board costs a thumbnail render, and why. The single source of truth for + // both the gate in `requestOgRenderForEdit` and the `sharedState` blob on `persist_success`, so the + // telemetry cannot report a state that differs from the one the decision was made on. + // + // `deleted` is its own state rather than folded into `private` or `unknown`: it never renders, so + // counting its persists as either would corrupt the shared fraction this exists to measure. + // + // Both delete lanes count. A hard delete arrives as `appFileRecordDidDelete` and flips + // `documentInfo.deleted`; a soft delete (trash) arrives as an ordinary record update and only + // flips `isDeleted` on the cached row, leaving `documentInfo.deleted` false. The connection path + // already treats the two as one, and so does this. + private getBoardRenderState(): 'shared' | 'private' | 'unknown' | 'legacy' | 'deleted' { + if (!this.documentInfo.isApp) return 'legacy' + if (this.documentInfo.deleted || this._fileRecordCache?.isDeleted) return 'deleted' + const shared = this._fileRecordCache?.shared + if (shared === undefined) return 'unknown' + return shared ? 'shared' : 'private' + } + + // Decides when this board's thumbnail is due (see OgRenderDebouncer). This object contributes the + // clock and the durable alarm, and the alarm below IS the deadline rather than an approximation of + // one: every persist re-arms it, so an eviction loses the in-memory copy and nothing else. + private ogRenderDebouncer = new OgRenderDebouncer() + + private scheduleOgRender() { + this.ctx.storage + .setAlarm(this.ogRenderDebouncer.onPersist(Date.now())) + .catch((e) => this.reportError(e)) + } + + // Rethrown rather than swallowed, unlike the socket handlers: the runtime retries an alarm that + // rejects, and dropping the error here would silently cancel the OG render this alarm exists to + // perform. Reporting only adds the Sentry record it was missing. + override async alarm(alarmInfo?: AlarmInvocationInfo) { + // A delete wipes documentInfo but not the pending og-render alarm, so this can fire on an + // object with nothing left to render. + if (!this._documentInfo || this._documentInfo.deleted) return + try { + // One clock reading for the whole fire: the debounce decision and the pending marker's + // expiry both count from it (see `firedAt` on enqueueOgImageRender). + const firedAt = Date.now() + const result = this.ogRenderDebouncer.onAlarm(firedAt) + if (!result.render) { + await this.ctx.storage.setAlarm(result.reArmAt) + return + } + await this.requestOgRenderForEdit(firedAt) + } catch (e) { + this.reportError(e, { source: 'alarm', retryCount: alarmInfo?.retryCount ?? 0 }) + throw e + } + } + + /** + * Indexes every data point on this object's durable object id, so any event can be grouped by + * room. The id is the one Cloudflare keys its own telemetry on — `$workers.durableObjectId` in + * Workers Logs, and the per-object filter on the namespace's metrics — so a room's analytics, + * logs, traces and CPU/storage numbers all line up on a single value. + * + * Deliberately the id rather than the slug: `idFromName` is one-way, so an id read out of the + * dataset does not open the board, where the slug for an app file is the whole authority of + * `tldraw.com/f/`. Resolving in the useful direction still works from a slug you already + * hold, via `env.TLDR_DOC.idFromName('/r/' + slug)`. + * + * Analytics Engine allows exactly one index, so this is the only object-level dimension these + * events carry. + */ + private writeEvent(name: string, eventData: EventData) { + writeDataPoint(this.sentry, this.measure, this.env, name, { + ...eventData, + indexes: [this.id.toString()], + }) + } + + logEvent(event: TLServerEvent) { + switch (event.type) { + case 'persist_success': { + // This event fires on exactly what triggers a thumbnail render, so it carries what sizing + // that spend needs. `writeEvent` already indexes it on the durable object id, which makes + // distinct boards countable; `sharedState` is the other half, and has to be recorded here + // because the id is one-way and cannot be joined back to a file row. + this.writeEvent(event.type, { + blobs: [event.sharedState], + doubles: [event.attempts], + }) + break + } + case 'room': { + if (event.name === 'room_start') { + this.writeEvent(event.name, { + doubles: [event.resumedSockets], + blobs: this.bootLoadIdBlobs(), + }) + } else { + this.writeEvent(event.name, {}) + } + break + } + case 'client': { + if (event.name === 'rate_limited') { + this.writeEvent(event.name, { blobs: [event.userId ?? 'anon-user'] }) + } else if (event.name === 'enter') { + // blob order is positional per event name. '0' covers bundles that predate the + // `?v=` connect param (and clients that sent a bogus one): it stays a number, so + // it sorts as the oldest possible build under both string and numeric ordering. + this.writeEvent(event.name, { + blobs: [event.instanceId, event.clientBuildTimestamp ?? '0'], + }) + } else { + this.writeEvent(event.name, { blobs: [event.instanceId] }) + } + break + } + case 'send_message': { + this.writeEvent(event.type, { + blobs: [event.messageType], + doubles: [event.messageLength], + }) + break + } + case 'version_chain_write': { + // '' rather than a shorter blobs array for deltas: the dataset's blob2 column keeps + // one meaning across both arms. + this.writeEvent(event.type, { + blobs: [event.wrote, event.wrote === 'keyframe' ? event.reason : ''], + doubles: [event.bytes, event.depth], + }) + break + } + case 'version_chain_verify': { + this.writeEvent(event.type, { + blobs: [event.outcome, event.outcome === 'ok' ? '' : event.reason], + doubles: event.outcome === 'skipped' ? [event.keyframeBytes] : [], + }) + break + } + case 'version_chain_error': { + this.writeEvent(event.type, {}) + break + } + default: { + exhaustiveSwitchError(event) + } + } + } + + async handleFileCreateFromSource(): Promise { + assert(this._fileRecordCache, 'we need to have a file record to create a file from source') + + const fetchTimer = this.timer() + const data = await this.loadCreateSourceData(this._fileRecordCache.createSource) + fetchTimer.report('create_from_source_fetch_total') + + if (!data) { + throw new RoomNotFoundError(this._fileRecordCache.id) + } + + const serialized = typeof data === 'string' ? data : JSON.stringify(data) + const snapshot = typeof data === 'string' ? JSON.parse(data) : data + + this.setBootStage('source-r2-put') + const putTimer = this.timer() + const key = getR2KeyForRoom({ slug: this._fileRecordCache.id, isApp: true }) + const roomObject = await this.r2.rooms.put(key, serialized) + putTimer.report('create_from_source_r2_put') + + return { + snapshot, + roomSizeMB: roomObject ? roomObject.size / MB : 0, + } + } + + /** + * Seed the room from its `createSource` and merge in Postgres comments. `createSource` is + * never cleared from the file record, so this re-runs whenever the R2 blob is missing, + * including a from-source file that gained comments and then lost its DO SQLite before the + * first throttled R2 persist; without the merge those comments would be orphaned (visible in + * /comments, absent from the room). A fresh duplicate has zero rows and the merge is a no-op. + */ + private async loadFromCreateSource( + commentsPromise: Promise | null + ): Promise { + const createFromSourceTimer = this.timer() + const res = await this.handleFileCreateFromSource() + createFromSourceTimer.report('db_load_create_from_source') + + if (commentsPromise) { + // Clone: loadCreateSourceData can return the shared DEFAULT_INITIAL_SNAPSHOT constant + // and the merge mutates top-level snapshot fields. + const snapshot: RoomSnapshot = { ...res.snapshot } + mergeCommentDocumentsIntoSnapshot(snapshot, await this.awaitComments(commentsPromise)) + res.snapshot = snapshot + } + + return res + } + + // The stage is set at the await, not the kickoff: the kickoff stage is overwritten by + // storage-load:r2 right after, so a hung Postgres dial would report as :r2 (#10746). + private async awaitComments(commentsPromise: Promise) { + this.setBootStage('storage-load:comments') + return await commentsPromise + } + + /** + * Resolve the seed content for a file's `createSource`, as a RoomSnapshot or its serialized + * string. Returns undefined for an unknown source, which the caller turns into RoomNotFoundError. + */ + private async loadCreateSourceData( + createSource: string | null | undefined + ): Promise { + // A new workspace's first file: a fixed marker (no prefix/id) the worker resolves to the + // welcome template's content, or a committed default — see resolveWelcomeSnapshot. + if (createSource === WELCOME_CREATE_SOURCE) { + this.setBootStage('source-welcome') + return await resolveWelcomeSnapshot(this.env, (e) => this.reportError(e)) + } + + const split = createSource?.split('/') + if (!split || split.length !== 2) { + throw new RoomNotFoundError(String(createSource)) + } + const [prefix, id] = split + switch (prefix) { + case FILE_PREFIX: { + // The source file's content is copied verbatim into this (user-owned) room. Read + // access to the source `id` is authorized upstream when the file record is created + // (see the `createFile` mutator), since that is where the user's identity is known. + // Bound the wait: a busy source room can hold its serial persist queue longer than + // the outbox drain's 30s effect timeout, wedging this boot and parking the insert + // effect (#10541). Its last persisted snapshot is typically at most one persist + // throttle stale — older only when the source's own persists are failing, which + // is reported separately. + this.setBootStage('source-await-persist') + const awaitPersistTimer = this.timer() + const persistWait = await settleWithin( + getRoomDurableObject(this.env, id).awaitPersist(), + SOURCE_PERSIST_WAIT_TIMEOUT_MS + ) + if (persistWait === 'timeout') { + this.reportError(new SourcePersistTimeoutError(id, SOURCE_PERSIST_WAIT_TIMEOUT_MS)) + } + awaitPersistTimer.report('create_from_source_await_persist') + this.setBootStage('source-r2-fetch') + + const r2FetchTimer = this.timer() + const text = await this.r2.rooms + .get(getR2KeyForRoom({ slug: id, isApp: true })) + .then((r) => r?.text()) + r2FetchTimer.report('create_from_source_r2_fetch') + return text + } + case ROOM_PREFIX: + this.setBootStage('source-legacy') + return await getLegacyRoomData(this.env, id, ROOM_OPEN_MODE.READ_WRITE) + case READ_ONLY_PREFIX: + this.setBootStage('source-legacy') + return await getLegacyRoomData(this.env, id, ROOM_OPEN_MODE.READ_ONLY) + case READ_ONLY_LEGACY_PREFIX: + this.setBootStage('source-legacy') + return await getLegacyRoomData(this.env, id, ROOM_OPEN_MODE.READ_ONLY_LEGACY) + case SNAPSHOT_PREFIX: + this.setBootStage('source-legacy') + return await getLegacyRoomData(this.env, id, 'snapshot') + case PUBLISH_PREFIX: + this.setBootStage('source-published') + return await getPublishedRoomSnapshot(this.env, id) + case LOCAL_FILE_PREFIX: + // create empty room, the client will populate it + return DEFAULT_INITIAL_SNAPSHOT + default: + return undefined + } + } + + // Load the room's drawing data. First we check the R2 bucket, then we fallback to supabase (legacy). + async loadFromDatabase(slug: string): Promise { + const loadTimer = this.timer() + this._bootTimings = {} + try { + const key = getR2KeyForRoom({ slug, isApp: this.documentInfo.isApp }) + + // Kick off the Postgres comments load now so it overlaps with the R2 fetch below; + // only the merge points further down actually await it. + const commentsPromise = this.documentInfo.isApp ? this.loadCommentsFromPostgres() : null + // Prevent an unhandled rejection if we exit via a path that never merges (e.g. + // RoomNotFoundError). Merge points still await commentsPromise itself, so a Postgres + // failure there still fails the room open. + commentsPromise?.catch(() => {}) + + // when loading, prefer to fetch documents from the bucket + const r2FetchTimer = this.timer() + this.setBootStage('storage-load:r2') + const roomFromBucket = await this.r2.rooms.get(key) + this._bootTimings.r2 = r2FetchTimer.report('db_load_r2_fetch', this.bootLoadIdBlobs()) + + if (roomFromBucket) { + const snapshot = (await roomFromBucket.json()) as RoomSnapshot + + // Object-lane records (comments) live in Postgres, the sole durable comment store + // (see drainCommentOutbox). Load them and merge them back into the snapshot so they + // seed the room and hydrate to clients on connect. A Postgres failure here fails the + // room open (bubbling like an R2 failure) — silently opening without comments would + // let the next persist treat them as deleted. + if (commentsPromise) { + mergeCommentDocumentsIntoSnapshot(snapshot, await this.awaitComments(commentsPromise)) + } + + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + + return { + snapshot, + roomSizeMB: roomFromBucket.size / MB, + } + } + + if (this._fileRecordCache?.createSource) { + const res = await this.loadFromCreateSource(commentsPromise) + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + return res + } + + if (this.documentInfo.isApp) { + // finally check whether the file exists in the DB but not in R2 yet + this.setBootStage('storage-load:file-record') + const file = await this.getAppFileRecord() + + if (!file) { + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + throw new RoomNotFoundError(slug) + } + + // The cache can still have been empty at the check above when the row only became + // visible on this second lookup (a connect that raced the createFile mutation). + // Seeding a from-source file with the empty default would persist an empty room, + // and the R2 blob then means `createSource` is never consulted again. + if (file.createSource) { + const res = await this.loadFromCreateSource(commentsPromise) + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + return res + } + + // Comments can exist in Postgres before the first throttled R2 persist ever runs + // (e.g. DO SQLite lost right after commenting on a fresh file), so rehydrate them + // here too. Clone the shared DEFAULT_INITIAL_SNAPSHOT constant — the merge reassigns + // `documents` and clamps clocks, and must not mutate the module-level object. + const snapshot: RoomSnapshot = { ...DEFAULT_INITIAL_SNAPSHOT } + const comments = await this.awaitComments(assertExists(commentsPromise)) + mergeCommentDocumentsIntoSnapshot(snapshot, comments) + + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + + return { + snapshot, + roomSizeMB: 0, + } + } + + // if we don't have a room in the bucket, try to load from supabase + const supabaseClient = this.supabaseClient + if (!supabaseClient) { + throw new RoomNotFoundError(slug) + } + + const supabaseFetchTimer = this.timer() + this.setBootStage('storage-load:supabase') + const { data, error } = await supabaseClient + .from(this.supabaseTable) + .select('*') + .eq('slug', slug) + + supabaseFetchTimer.report('db_load_supabase_fetch') + + if (error) { + this.logEvent({ type: 'room', name: 'failed_load_from_db' }) + + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + + console.error('failed to retrieve document', slug, error) + throw new Error(error.message) + } + // if it didn't find a document, data will be an empty array + if (data.length === 0) { + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + throw new RoomNotFoundError(slug) + } + + const roomFromSupabase = data[0] as PersistedRoomSnapshotForSupabase + this._bootTimings.total = loadTimer.report('db_load_total', this.bootLoadIdBlobs()) + + return { + snapshot: roomFromSupabase.drawing, + roomSizeMB: 0, + } + } catch (error) { + this.logEvent({ type: 'room', name: 'failed_load_from_db' }) + + loadTimer.report('db_load_total_error', this.bootLoadIdBlobs()) + + console.error('failed to fetch doc', slug, error) + throw error + } + } + + private async loadCommentsFromPostgres(): Promise { + // Timed here rather than at the merge-point await so the event is query latency, not the + // residual wait after the overlapping R2 fetch (which would read ~0 whenever R2 is slower). + const commentsTimer = this.timer() + const result = await loadCommentDocuments(this.db, this.documentInfo.slug) + this._bootTimings.comments = commentsTimer.report('db_load_comments', this.bootLoadIdBlobs()) + return result + } + + // The load id of the request that booted this room, so the boot-time timers (db_load_*, + // room_start) can be joined to that client's first_load event. Later connects don't boot. + private _bootLoadId: string | undefined + + // Request timers take the connecting client's own id; boot timers take the id of the client + // whose connect booted the room. Kept separate on purpose: a request without an id must not + // borrow the booting visitor's. + private loadIdBlobs(loadId: string | undefined): string[] | undefined { + return loadId ? [loadId] : undefined + } + + private bootLoadIdBlobs(): string[] | undefined { + return this.loadIdBlobs(this._bootLoadId) + } + + timer() { + const start = Date.now() + return { + // blobs are appended after the event name and worker name; each timer event's positional + // layout is otherwise unchanged, so a load id in blob3 is safe for existing queries. + report: (name: string, blobs?: string[]) => { + const ms = Date.now() - start + this.writeEvent(name, { doubles: [ms], blobs }) + return ms + }, + } + } + + // Stage durations of the most recent storage load, echoed to the client that booted the room. + private _bootTimings: { r2?: number; comments?: number; total?: number } = {} + + // first_load_server messages waiting for their session's connect handshake to complete. + private _pendingFirstLoadEchoes = new Map() + + // Called wherever the room is dropped. A reopen from retained storage skips loadFromDatabase + // (the only writer), so without this the next booting client would be echoed the old numbers. + private dropBootTimings() { + this._bootTimings = {} + this._bootLoadId = undefined + } + + _lastPersistedClock: number | null = null + + // Fingerprint of the snapshot known to be in the rooms bucket, read from its customMetadata so + // a cold start doesn't re-upload a board nobody edited. The clock guard above can't do this — + // it dies with the isolate, and object-lane (comment) writes bump it without changing the + // document snapshot. + // + // undefined means "not looked up yet" and null means "looked up, no usable stamp"; the two are + // distinct because the lookup must happen at most once (see resolvePersistedFingerprint). + _lastPersistedFingerprint: SnapshotFingerprint | null | undefined = undefined + + // Serializes comment outbox drains (and the restore-path deletes) so they land in order. + // Separate from executionQueue (the R2/main-persist queue) since these pushes fire immediately + // on commit, not on the throttle. + private _objectPushQueue = new ExecutionQueue() + + executionQueue = new ExecutionQueue() + + private getUserContentUrl(): string { + return assertExists(this.env.USER_CONTENT_URL, 'USER_CONTENT_URL is required') + } + + private readonly associateAssetsQueue = new ExecutionQueue() + + // Source objects the association pass confirmed missing from the uploads bucket. Skipped on + // later passes so an asset pointing at a gone object doesn't re-attempt an R2 get on every + // persist tick. In-memory only: resets with the DO, never touches document data. + private readonly missingSourceObjects = new Set() + + // Shared connection budget for every R2 operation this durable object makes. Both asset copies and + // snapshot uploads draw from this queue so together they can't exceed Cloudflare's simultaneous- + // connection limit (see MAX_CONCURRENT_R2_OPERATIONS). + private readonly r2Queue = new PQueue({ concurrency: MAX_CONCURRENT_R2_OPERATIONS }) + + // Wraps a queued R2 task so the queue's depth is counted against `type` from submit until the task + // settles. While the queue is backed up it writes a per-type depth metric for Grafana, and a + // runaway backlog reports to Sentry once per spike (re-arming after the queue drains). Depth and + // the alert flag live in this closure rather than on the durable object. + private readonly trackQueuedTask = (() => { + const depthByType = new Map() + let alerted = false + let emitScheduled = false + const totalDepth = () => { + let total = 0 + for (const depth of depthByType.values()) total += depth + return total + } + // A single association pass submits all of its copies synchronously, so depth jumps straight + // to the batch size. Coalesce that burst into one deferred emit that captures the peak, rather + // than writing an analytics point per submitted task (which would flood Analytics Engine). + const scheduleEmit = () => { + if (emitScheduled) return + emitScheduled = true + queueMicrotask(() => { + emitScheduled = false + const total = totalDepth() + if (total < R2_QUEUE_DEPTH_METRIC_THRESHOLD) return + for (const [type, depth] of depthByType) { + this.writeEvent('r2_queue_depth', { blobs: [type], doubles: [total, depth] }) + } + if (total >= R2_QUEUE_DEPTH_ALERT_THRESHOLD && !alerted) { + alerted = true + this.reportError( + new Error( + `R2 connection queue depth reached ${total} (>= ${R2_QUEUE_DEPTH_ALERT_THRESHOLD}) for file ${this.documentInfo.slug}` + ) + ) + } + }) + } + return (type: R2OperationType, task: () => Promise): (() => Promise) => { + depthByType.set(type, (depthByType.get(type) ?? 0) + 1) + if (totalDepth() < R2_QUEUE_DEPTH_METRIC_THRESHOLD) { + alerted = false + } else { + scheduleEmit() + } + return async () => { + try { + return await task() + } finally { + const depth = (depthByType.get(type) ?? 1) - 1 + if (depth <= 0) depthByType.delete(type) + else depthByType.set(type, depth) + } + } + } + })() + + // Submits an R2 operation to the shared connection budget (see r2Queue). Snapshot uploads run at a + // higher priority than asset copies so persistence isn't stuck behind a large association pass's + // backlog of copies — durability shouldn't wait on background asset work. + private addR2Operation(type: R2OperationType, task: () => Promise) { + return this.r2Queue.add(this.trackQueuedTask(type, task), { + // Both sit on the serial persist path; neither may wait behind an asset_copy backlog. + priority: type === 'snapshot_upload' || type === 'version_chain_write' ? 1 : 0, + }) + } + + // Associates every asset in this (app) file with the file. Needed for cases like duplicating a + // file, copy-pasting images between files, and slurping legacy files; also migrates old-format + // asset URLs to tldrawusercontent.com. Only one pass runs at a time; concurrent calls are dropped + // because the running pass already drains the whole store (see associateFileAssets). + async maybeAssociateFileAssets() { + if (!this.documentInfo.isApp) return + if (!this.associateAssetsQueue.isEmpty()) return + await this.associateAssetsQueue.push(() => this.associateFileAssets()) + } + + private async associateFileAssets() { + // Keep going until there's nothing left to associate. Copying takes a while and more assets can + // arrive in the meantime, so the running pass keeps draining rather than relying on a later + // trigger to pick up the stragglers. This runs un-awaited from persist ticks, so a failed pass + // is reported and ends the drain rather than surfacing as an unhandled rejection. + while (true) { + let associated: number + try { + associated = await this.associatePendingAssets() + } catch (e) { + this.reportError(e) + return + } + if (!associated) return + } + } + + // Associates every asset that isn't linked to this file yet: copies it to a new object owned by the + // file and repoints the asset at it (and migrates old-format URLs in place). Returns how many + // assets it associated — 0 means there's nothing left to do, or nothing it can make progress on + // (e.g. an asset whose source object is missing), which is what stops the loop above. + private async associatePendingAssets(): Promise { + const slug = this.documentInfo.slug + const storage = await this.getStorage() + const userContentUrl = this.getUserContentUrl() + + const { + result: { assetsToReplace, assetsToMigrate }, + } = storage.transaction((txn) => + collectAssetAssociationChanges(txn.values(), { + slug, + userContentUrl, + skipObjectNames: this.missingSourceObjects, + }) + ) + + // Apply URL migrations (no R2 copy needed — same bucket, same object) + if (assetsToMigrate.length > 0) { + storage.transaction((txn) => { + for (const migration of assetsToMigrate) { + const assetRecord = txn.get(migration.assetId) as TLAsset | undefined + if (!assetRecord) continue + assetRecord.props.src = migration.newSrc + txn.set(migration.assetId, assetRecord) + } + }) + } + + if (assetsToReplace.length === 0) return 0 + + const rows: { objectName: string; fileId: string }[] = [] + await Promise.allSettled( + assetsToReplace.map((asset) => + this.addR2Operation('asset_copy', async () => { + try { + const currentAsset = await this.env.UPLOADS.get(asset.objectName) + if (!currentAsset) { + // Only a confirmed null (object genuinely absent) goes in the cache — a + // thrown error (rate limit, subrequest budget) must stay retryable + this.missingSourceObjects.add(asset.objectName) + return + } + await this.env.UPLOADS.put(asset.newObjectName, currentAsset.body, { + httpMetadata: currentAsset.httpMetadata, + }) + + storage.transaction((txn) => { + const assetRecord = txn.get(asset.assetId) as TLAsset | undefined + if (!assetRecord) return // extremely unlikely, not sure why this would happen + assetRecord.props.src = asset.newSrc + assetRecord.meta.fileId = slug + txn.set(asset.assetId, assetRecord) + }) + + rows.push({ + objectName: asset.newObjectName, + fileId: slug, + }) + } catch (e) { + this.reportError(e) + } + }) + ) + ) + + // Nothing copied, so there's no progress to be made. + if (rows.length === 0) return 0 + + await this.db + .insertInto('asset') + .values(rows) + .onConflict((oc) => { + return oc.column('objectName').doUpdateSet({ fileId: slug }) + }) + .execute() + + return rows.length + } + + protected async setRoomStorageUsedPercentage(roomSizeMB: number) { + const storage = await this.getStorage() + const percentage = Math.ceil((roomSizeMB / ROOM_SIZE_LIMIT_MB) * 100) + storage.transaction((txn) => { + const document = txn.get(TLDOCUMENT_ID) as TLDocument + const meta = document.meta + if (meta.storageUsedPercentage === percentage) return + // In some cases we don't want to update the document if it already has percentage set. + // Example for that is when we load the room. If it has a percentage set, we don't want to overwrite it. + txn.set(TLDOCUMENT_ID, { ...document, meta: { ...meta, storageUsedPercentage: percentage } }) + }) + } + + broadcastPersistenceEvent(event: TLCustomServerEvent) { + this._room?.then((r) => { + for (const session of r.getSessions()) { + r.sendCustomMessage(session.sessionId, event) + } + }) + } + persistenceBad = false + + // Save the room to r2 + async persistToDatabase(opts?: { throwOnFailure?: boolean }) { + await this.executionQueue + .push(async () => { + await retry( + async ({ attempt }) => { + if (attempt === PERSIST_RETRIES_NOTIFY_THRESHOLD && !this.persistenceBad) { + this.broadcastPersistenceEvent({ type: 'persistence_bad' }) + this.persistenceBad = true + } + // check whether the worker was woken up to persist after having gone to sleep + if (!this._room) return + // a deleted room's R2 keys are gone; persisting would resurrect them + if (this._documentInfo?.deleted) return + const slug = this.documentInfo.slug + const storage = await this.getStorage() + assert(storage instanceof SQLiteSyncStorage, 'storage must be a SQLiteSyncStorage') + // Object-lane records (comments) persist to Postgres via the outbox; the + // throttled persist just nudges a drain so pushes that failed earlier get + // retried. This must run BEFORE the clock early-return below: a quiet room + // (no new commits) would otherwise never retry a failed drain. Fire-and-forget: + // a Postgres outage must not fail the R2 document persist — the outbox keeps + // the pending work. + this.drainCommentOutbox() + if (this._lastPersistedClock === storage.getClock()) return + if (this._isRestoring) return + + // The storage partitions object-lane records (comments) into their own store, so + // the document snapshot is pure-document by construction — no splitting needed. + const snapshot = storage.getSnapshot() + assert(snapshot.documentClock !== undefined, 'documentClock must be present') + this.maybeAssociateFileAssets() + + const key = getR2KeyForRoom({ slug: slug, isApp: this.documentInfo.isApp }) + const snapshotFingerprint = getSnapshotFingerprint(snapshot) + const persistedFingerprint = await resolvePersistedFingerprint( + this._lastPersistedFingerprint, + this.r2.rooms, + key + ) + this._lastPersistedFingerprint = persistedFingerprint + if (isSameFingerprint(persistedFingerprint, snapshotFingerprint)) { + // The clock moved but neither the document nor the schema did — a comment + // write, or a cold start re-checking work a previous incarnation already + // persisted. Uploading would store byte-identical content under a new + // history timestamp and re-render an unchanged thumbnail; skip both, but + // keep the rest of a persist's observable behavior. + this._lastPersistedClock = snapshot.documentClock + this.markPersistenceGood() + this.bumpFileUpdatedAt() + return + } + await this._uploadSnapshotToR2(snapshot, key) + this._lastPersistedFingerprint = snapshotFingerprint + + this.logEvent({ + type: 'persist_success', + attempts: attempt, + sharedState: this.getBoardRenderState(), + }) + this._lastPersistedClock = snapshot.documentClock + // The board's content just changed, so its thumbnail is out of date. Push the render + // deadline out rather than rendering now: the useful thumbnail is of the settled + // board, and a persist mid-session says more edits are probably coming. Costs one + // alarm write per persist, not awaited here (see scheduleOgRender), so a slow or + // failed write cannot hold up a persist. + this.scheduleOgRender() + this.markPersistenceGood() + + this.bumpFileUpdatedAt() + }, + { + // throwOnFailure callers (publish) are awaited inside a 30s outbox effect + // timeout; the default 100 attempts (~200s) would blow past it, surfacing as a + // generic timeout with the real cause lost. Cap those callers' own retries well + // under it. Not a hard bound: a best-effort persist already queued ahead on the + // serial executionQueue can still push the wait past the timeout - the effect + // timeout covers that, and since the retried effect queues behind the same + // task and the clock check makes it a no-op, an overlap is harmless. + attempts: opts?.throwOnFailure ? PERSIST_RETRIES_MAX_THROWING : PERSIST_RETRIES_MAX, + waitDuration: 2000, + } + ) + }) + .catch((e) => { + this.logEvent({ type: 'room', name: 'fail_persist' }) + this.reportError(e) + if (opts?.throwOnFailure) throw e + }) + } + + /** + * Asks for this board's thumbnail to be re-rendered, because the content it depicts just changed. + * + * Called from `alarm()` when the debounce expires, so it runs once editing has settled or the max + * wait is up. That debounce is the render rate control; there is no sampling or staleness gate on + * top, because a persist means the saved content genuinely differs from what the cached thumbnail + * shows. Downstream, the pending marker single-flights the ask and the consumer re-checks + * `(board, version)` before spending a Browser Run slot — neither is a rate control either. + */ + private async requestOgRenderForEdit(firedAt: number) { + try { + // Two states skip, and neither is about privacy: a legacy room is not an app file and has no + // board identity to render, and a deleted one has nothing worth depicting. `shared`, `private` + // and `unknown` all proceed — every board gets a thumbnail, so that an owner-facing surface has + // one to show. Sharing is a condition of *serving*, re-applied by the OG route per request. + const state = this.getBoardRenderState() + if (state === 'legacy' || state === 'deleted') return + + const slug = this.documentInfo.slug + const result = await enqueueOgImageRender( + this.env, + { kind: 'shared_file', slug }, + { reason: 'edit', firedAt } + ) + // No board identifier: for a shared file the slug is a capability, and a derived id is still a + // board identity in a log sink. The result alone is what this line is for. + this.log.debug('og render for edit', result) + } catch (e) { + // Reported, not thrown: this runs off the persist path and must never affect it. + this.reportError(e) + } + } + + // Clears the "persistence bad" banner clients are showing. Also called when a persist skips + // its uploads: a skip is a successful persist, and the banner would otherwise stay up until + // the isolate dies on a board whose only remaining writes are comments. + private markPersistenceGood() { + if (!this.persistenceBad) return + this.broadcastPersistenceEvent({ type: 'persistence_good' }) + this.persistenceBad = false + } + + // Updates the file's updatedAt timestamp. Also called when a persist skips its uploads, + // because comment-only activity should still surface as file activity. + private bumpFileUpdatedAt() { + if (!this.documentInfo.isApp) return + // don't await on this because otherwise + // if this logic is invoked during another db transaction + // (e.g. when publishing a file) + // that transaction will deadlock + this.db + .updateTable('file') + .set({ updatedAt: new Date().getTime() }) + .where('id', '=', this.documentInfo.slug) + .execute() + .catch((e) => { + this.logEvent({ + type: 'room', + name: 'failed_persist_to_db', + }) + this.reportError(e) + }) + } + + private async _uploadSnapshotToR2(snapshot: RoomSnapshot, key: string) { + const customMetadata = getSnapshotMetadata(snapshot) + // Per attempt, not per persist: the retry loop re-reads the snapshot, so a retry may carry + // newer content that deserves its own version. When the chain already holds THIS content + // from an earlier attempt, _writeVersionChainEntry hands back the key it used, and the + // legacy copy lands under that same key — the two buckets must agree on when a version is. + let iso = new Date().toISOString() + // Upload to rooms bucket first + const roomSizeMB = await this._uploadSnapshotToBucket( + this.r2.rooms, + snapshot, + key, + customMetadata + ) + // Update storage percentage + if (roomSizeMB !== null) { + await this.setRoomStorageUsedPercentage(roomSizeMB) + } + + const mode = resolveVersionChainMode(await this.versionChainRollout(), key) + + // A non-transient chain failure (corrupt segment, a 4xx, DO storage) must be a metric, not + // a failed persist: the outer retry would otherwise re-upload the rooms object 100 times, + // raise persistence_bad, and record no version at all. In dual mode the legacy write below + // runs regardless; in chain mode it runs as the fallback, so the version still exists as + // a full copy the read paths already know how to serve. + let chainWritten = false + if (mode !== 'off') { + try { + iso = await this._writeVersionChainEntry(snapshot, key, iso) + chainWritten = true + } catch (error) { + this.logEvent({ type: 'version_chain_error' }) + this.reportError(error) + } + } + // Dual-write keeps the legacy full copy as the independent record the read-path verifier + // checks chain reconstructions against. (_verifyRetiredChain only compares against what this + // DO last persisted.) Stage 3 of the rollout flips this to 'chain'. + // Nothing dedupes this write the way the version check in persistToDatabase does: a retry + // that got here has already set _lastPersistedFingerprint and takes the skip path instead + // (the chain write above carries its own re-entry guard for the same reason). + if (mode !== 'chain' || !chainWritten) { + await this._uploadSnapshotToBucket( + this.r2.versionCache, + snapshot, + `${key}/${iso}`, + customMetadata + ) + } + } + + /** + * Writes this snapshot into the chain and returns the version key (ISO timestamp) it lives under + * — `iso` when written now, or the key from an earlier attempt when the chain already holds + * exactly this content. + */ + private async _writeVersionChainEntry( + snapshot: RoomSnapshot, + key: string, + iso: string + ): Promise { + let chain = await this.getVersionChain() + // Re-entry guard: a dual-write persist that failed on the legacy upload retries this whole + // method with the chain already holding this exact version. Without it, every such retry + // appends a no-op delta at a fresh timestamp — the duplicate class #10571 exists to kill. + // Head identity, not just the fingerprint: a tombstone prune between attempts keeps the + // fingerprint but changes content, and the legacy copy must not land under a key the chain + // holds other content at. + if (chain && isChainHead(chain, snapshot)) { + return this._versionChainHeadIso ?? iso + } + let pending: PendingDelta[] = [] + let noChainReason: 'segment-lost' | undefined + if (chain) { + const rehydrated = await this.getPendingDeltas(chain) + // The chain said a segment was open but R2 no longer has it. Appending would rewrite the + // segment without the deltas its metadata still promises, so start a fresh chain instead. + // The keys go to the log, not the metric: analytics blobs carry no R2 keys. + if (rehydrated === null) { + console.error( + `Version chain lost its open segment; cutting a keyframe. room=${key} segment=${chain.openSegment?.key}` + ) + noChainReason = 'segment-lost' + chain = null + } else { + pending = rehydrated + } + } + // R2 persist flakiness is a known quantity (see the multipart/fallback machinery on the + // snapshot uploads). A chain write is one idempotent PUT for a fixed iso, so retrying the + // whole call is safe. + const result = await this.addR2Operation('version_chain_write', () => + retry( + () => + writeVersionChainEntry({ + bucket: this.r2.versionChain, + roomKey: key, + iso, + chain, + noChainReason, + pending, + previous: this._lastPersistedSnapshot, + previousHeadHash: this._lastPersistedHeadHash ?? undefined, + next: snapshot, + now: Date.now(), + }), + { attempts: 3, waitDuration: 500, matchError: isTransientConnectionError } + ) + ) + this._versionChain = result.chain + this._pendingDeltas = result.pending + this._versionChainHeadIso = iso + await this.storage.put(VERSION_CHAIN_STORAGE_KEY, result.chain) + const previous = this._lastPersistedSnapshot + this._lastPersistedSnapshot = snapshot + this._lastPersistedHeadHash = result.chain.headHash + this.logEvent({ + type: 'version_chain_write', + bytes: result.bytes, + depth: result.chain.deltaCount, + ...(result.wrote === 'keyframe' + ? { wrote: result.wrote, reason: result.reason } + : { wrote: result.wrote }), + }) + // A cadence keyframe retires a complete chain that should reproduce `previous` exactly. + // Prove it while both sides are cheap to compare — this keeps the bake's verification + // running for the life of the system, not just the rollout. Off the persist path, and a + // failure is a metric, never an error: the keyframe just cut already healed the chain. + if ( + result.wrote === 'keyframe' && + (result.reason === 'delta-count' || result.reason === 'chain-age') && + chain && + previous && + pending.length > 0 + ) { + // Skipped, not deferred: reconstructing a big chain is the one thing on this path that can + // take the live room down with it (see MAX_VERIFY_KEYFRAME_BYTES), and there is no later + // moment where the object holds fewer copies of the board. + if (chain.keyframeBytes > MAX_VERIFY_KEYFRAME_BYTES) { + this.logEvent({ + type: 'version_chain_verify', + outcome: 'skipped', + reason: 'keyframe-size', + keyframeBytes: chain.keyframeBytes, + }) + } else { + this.ctx.waitUntil(this._verifyRetiredChain(pending[pending.length - 1].t, previous)) + } + } + return iso + } + + private async _verifyRetiredChain(lastDeltaTimestamp: string, expected: RoomSnapshot) { + try { + // Each read is its own queued operation rather than the whole reconstruction holding one + // slot: a slot is sized for an asset copy's two connections, and reconstruction fans out to + // the keyframe plus every segment — five beside a copy is over the six. + const reconstruction = await reconstructVersion({ + chainBucket: this.r2.versionChain, + legacyBucket: this.r2.versionCache, + roomKey: getR2KeyForRoom(this.documentInfo), + timestamp: lastDeltaTimestamp, + schedule: (read) => this.addR2Operation('version_chain_verify', read), + }) + // A legacy full copy is not the chain reading back; only a chain answer counts. Compared on + // the head hash, not the envelope hash: `expected` may be the wake seed, whose documentClock a + // comment write moved past the clock the chain head was written at — a chain-age keyframe on + // an idle commented board is exactly that case, and it would fail here on a correct chain. + const reason = !reconstruction + ? 'missing' + : reconstruction.source !== 'chain' + ? 'legacy-fallback' + : chainHeadHash(reconstruction.snapshot) !== chainHeadHash(expected) + ? 'head-mismatch' + : null + this.logEvent( + reason === null + ? { type: 'version_chain_verify', outcome: 'ok' } + : { type: 'version_chain_verify', outcome: 'fail', reason } + ) + } catch (error) { + this.logEvent({ type: 'version_chain_verify', outcome: 'fail', reason: 'error' }) + this.reportError(error) + } + } + + private async _uploadSnapshotToBucket( + bucket: R2Bucket, + snapshot: RoomSnapshot, + key: string, + customMetadata: Record + ): Promise { + // Funnel through the shared connection budget so the upload can't contend with a concurrent + // asset-association pass (or the version-cache upload) and exhaust Cloudflare's connections. + const result = await this.addR2Operation('snapshot_upload', async () => { + try { + // Try multipart upload first, retrying transient connection drops before falling back. + // Only connection-type errors are worth retrying; anything else fails fast to the PUT + // fallback rather than sleeping between attempts. + return await retry( + () => this._uploadSnapshotToBucketMultipart(bucket, snapshot, key, customMetadata), + { + attempts: 3, + waitDuration: 500, + matchError: isTransientConnectionError, + } + ) + } catch (multipartError) { + // Falling back to a simple PUT is the designed recovery path, so it's a breadcrumb + // rather than a captured exception — only a failure of the fallback itself is reported. + // eslint-disable-next-line @typescript-eslint/no-deprecated + this.sentry?.addBreadcrumb({ + message: `Multipart upload failed, falling back to simple PUT: ${multipartError}`, + }) + try { + return await this._uploadSnapshotToBucketSimple(bucket, snapshot, key, customMetadata) + } catch (putError) { + this.reportError(putError) + throw putError + } + } + }) + return result ?? null + } + + private async _uploadSnapshotToBucketMultipart( + bucket: R2Bucket, + snapshot: RoomSnapshot, + key: string, + customMetadata: Record + ) { + const out = await bucket.createMultipartUpload(key, { customMetadata }) + + try { + // 5MB buffer + const fiveMB = 5 * MB + const buffer = new Uint8Array(fiveMB) + const parts: R2UploadedPart[] = [] + let partNumber = 1 + let offset = 0 + + const uploadBuffer = async (data: Uint8Array) => { + const part = await out.uploadPart(partNumber, data) + parts.push(part) + partNumber++ + } + + for (const chunk of generateSnapshotChunks(snapshot)) { + let remainingChunk = chunk + + while (remainingChunk.byteLength > 0) { + const spaceLeft = fiveMB - offset + const chunkToAdd = remainingChunk.subarray( + 0, + Math.min(spaceLeft, remainingChunk.byteLength) + ) + + buffer.set(chunkToAdd, offset) + offset += chunkToAdd.byteLength + + // If buffer is full, upload it + if (offset >= fiveMB) { + await uploadBuffer(buffer.subarray(0, offset)) + offset = 0 + } + + remainingChunk = remainingChunk.subarray(chunkToAdd.byteLength) + } + } + if (offset > 0) { + await uploadBuffer(buffer.subarray(0, offset)) + } + + const result = await out.complete(parts) + if (result) { + return result.size / MB + } + return null + } catch (e) { + await out.abort() + throw e + } + } + + private async _uploadSnapshotToBucketSimple( + bucket: R2Bucket, + snapshot: RoomSnapshot, + key: string, + customMetadata: Record + ) { + const serialized = JSON.stringify(snapshot) + const result = await bucket.put(key, serialized, { customMetadata }) + if (result) { + return result.size / MB + } + + return null + } + + /** + * Durable outbox for comment persistence. Postgres is the sole durable store for comment + * records; the DO's SQLite is the room's working copy. Each committed diff appends the touched + * comment record ids (comment, thread, or reaction) here (same-task with the commit, so the output gate + * flushes them together), and the drain pushes the records' current state to Postgres. The + * drain itself also appends: when the author-cascade prune (below) empties a thread, the + * pruned thread's id is outboxed so a follow-up drain deletes its Postgres row through this + * same acked path. + * Delivery is at-least-once: an entry's outbox row is only removed once its push to Postgres + * has actually succeeded, so a row that fails (including its row-by-row retry) stays queued + * and is retried on the next commit, the next throttled persist, or the drain kicked when + * storage loads on DO wake (see getStorage). A row that keeps failing stays queued + * indefinitely and keeps reporting via reportError on every drain — that's the visibility + * mechanism for a stuck row. Two exceptions clear without a successful push: a comment whose + * author's account was deleted (authorId FK violation — Postgres already cascaded the row + * away, so the drain prunes the record from the room instead of retrying; see + * drainCommentOutbox), and malformed/unknown outbox ids, where retrying can't help. + */ + private ensureCommentOutbox() { + this.ctx.storage.sql.exec( + 'CREATE TABLE IF NOT EXISTS comment_outbox (seq INTEGER PRIMARY KEY AUTOINCREMENT, recordId TEXT NOT NULL)' + ) + } + + private enqueueCommentChanges(diff: TLSyncForwardDiff) { + const ids: string[] = [] + for (const put of Object.values(diff.puts)) { + const record = (Array.isArray(put) ? put[1] : put) as { id: string } + if (isCommentRecordId(record.id)) ids.push(record.id) + } + for (const id of diff.deletes) { + if (isCommentRecordId(id)) ids.push(id) + } + if (ids.length === 0) return + this.ensureCommentOutbox() + for (const id of ids) { + this.ctx.storage.sql.exec('INSERT INTO comment_outbox (recordId) VALUES (?)', id) + } + this.drainCommentOutbox() + } + + /** + * Of a drain's planned mention reconciles, the ones whose comment this file actually owns. + * + * The comment upserts are fileId-guarded, so a record pushed under an id Postgres attributes to + * another file updates nothing — but it does so without erroring, so it still arrives here as a + * reconcile target. comment_mention is keyed on commentId alone, so reconciling one of those + * would rewrite the owning file's mention rows: the very thing the guard prevented on the comment + * row itself. A clock-guarded no-op replay still owns its comment, so filtering here can't strand + * a reconcile that an earlier crashed drain left undone. + */ + private async keepOwnedMentionReconciles( + reconciles: T[], + fileId: string + ): Promise { + if (reconciles.length === 0) return reconciles + const owned = new Set( + ( + await this.db + .selectFrom('comment') + .select('id') + .where( + 'id', + 'in', + reconciles.map((r) => r.commentId) + ) + .where('fileId', '=', fileId) + .execute() + ).map((row) => row.id) + ) + return reconciles.filter((r) => owned.has(r.commentId)) + } + + /** + * Push every outboxed record's current state to Postgres. The outbox stores only ids; whether + * an id is an upsert or a delete is decided by its presence in the object lane at drain time + * (see planCommentDrain), so multiple edits coalesce and a create-then-delete nets out to a + * delete. Upserts are clock-guarded: a replayed push (same lastChangedClock) updates nothing, + * producing no WAL entry and therefore no Zero replication churn. + * + * Each drain is bounded to the outbox high-water mark captured synchronously at SCHEDULE time, + * not at execution time: entries enqueued while the drain sat in the queue are left for their + * own drain (scheduled at enqueue, so its bound covers them). This is what lets the restore + * handler wipe the lane and delete the file's Postgres rows without a stale queued drain + * upserting — and clearing the outbox entry of — a comment committed after the wipe, which + * the restore's fileId-wide delete would then remove from Postgres with no retry left. + */ + private drainCommentOutbox(): Promise { + this.ensureCommentOutbox() + // sql.exec is synchronous, so the bound is captured before anything else can enqueue. + const drainBound = Number( + this.ctx.storage.sql + .exec('SELECT COALESCE(MAX(seq), 0) AS maxSeq FROM comment_outbox') + .toArray()[0].maxSeq + ) + return this._objectPushQueue + .push(async () => { + const entries = this.ctx.storage.sql + .exec('SELECT seq, recordId FROM comment_outbox WHERE seq <= ? ORDER BY seq', drainBound) + .toArray() as { seq: number; recordId: string }[] + if (entries.length === 0) return + + const storage = await this.getStorage() + assert(storage instanceof SQLiteSyncStorage, 'storage must be a SQLiteSyncStorage') + // Targeted reads, not a full snapshot: each lane read is a SELECT + JSON.parse, so + // snapshotting would make every drain O(all comments). Both reads run synchronously + // here, before any await, so `lane` keeps its snapshot semantics. + const lane = new Map( + storage + .getObjectsByIds(entries.map((e) => e.recordId)) + .map((doc) => [doc.state.id as string, doc]) + ) + const fileId = this.documentInfo.slug + // planCommentDrain checks each outboxed reaction's parent comment against the lane, and + // a parent needn't be outboxed itself (reacting doesn't touch the comment) — so fetch + // those too, still before any await. + const parentCommentIds = new Set() + for (const doc of lane.values()) { + if (!isCommentReactionId(doc.state.id as string)) continue + const { commentId } = doc.state as TLCommentReaction + if (commentId && !lane.has(commentId)) parentCommentIds.add(commentId) + } + for (const doc of storage.getObjectsByIds(parentCommentIds)) { + lane.set(doc.state.id as string, doc) + } + + const { + threadUpserts, + commentUpserts, + reactionUpserts, + threadDeletes, + commentDeletes, + reactionDeletes, + unknownIds, + orphanedReactionIds, + } = planCommentDrain(entries, lane, fileId) + // The prune predicate below asks `lane.has(threadId)`, and a parent thread needn't be + // outboxed itself — so fetch those too, still before any await. + const parentThreadIds = new Set() + for (const row of commentUpserts) { + if (!lane.has(row.threadId)) parentThreadIds.add(row.threadId) + } + for (const doc of storage.getObjectsByIds(parentThreadIds)) { + lane.set(doc.state.id as string, doc) + } + if (orphanedReactionIds.length > 0) { + // The parent comment isn't in this room, so the reaction can never render here — and + // must not reach Postgres under this file's id, because comment_reaction is joined to + // a comment by commentId, so the row would surface on whichever file does own that + // comment. Prune it from the lane the way an FK-violating reaction prunes below; its + // outbox entry clears normally, since neither a deleted parent nor a forged one + // resolves on a retry. + storage.transaction((txn) => { + for (const id of orphanedReactionIds) txn.delete(id as TLRecord['id']) + }) + } + for (const id of unknownIds) { + // enqueueCommentChanges only writes comment record ids, so an unknown + // id means a bug or a corrupted outbox row. Skip it — its entry still clears + // below (retrying can't help) — and report for visibility. + this.reportError(new Error(`comment outbox: unknown record id ${JSON.stringify(id)}`)) + } + + // The upsert builders live in commentRows.ts, where the fileId conflict guard they all + // carry is documented and unit-tested against the compiled SQL. + const insertThreadRows = (rows: DB['comment_thread'][]) => + upsertCommentThreadRows(this.db, rows).execute() + const insertCommentRows = (rows: DB['comment'][]) => + upsertCommentRows(this.db, rows).execute() + const insertReactionRows = (rows: DB['comment_reaction'][]) => + upsertCommentReactionRows(this.db, rows).execute() + + // Thread upserts before comment upserts (comment.threadId FK); comment deletes + // before thread deletes is not required (thread deletes cascade), but keep the + // batch → row-by-row fallback so one bad row doesn't drop the whole batch. Rows that fail both the + // batch insert and their individual retry are reported back (by record id) so the + // caller can keep their outbox entries queued instead of deleting them. Rows whose + // error matches `shouldPrune` are returned separately instead: they are not + // reported as failures and their outbox entries clear normally. + const runBatchWithFallback = async ( + rows: R[], + insert: (rows: R[]) => Promise, + // Called with the offending row on the row-by-row pass, and without one for the + // batch error (where it only decides whether to report — the per-row pass + // attributes the failure properly). Taking the row lets a predicate that can't + // judge an error in isolation — see the comment thread-FK case — look at what + // was actually being written. + shouldPrune?: (error: unknown, row?: R) => boolean + ): Promise<{ failedIds: string[]; prunedIds: string[] }> => { + const failedIds: string[] = [] + const prunedIds: string[] = [] + if (rows.length === 0) return { failedIds, prunedIds } + try { + await insert(rows) + return { failedIds, prunedIds } + } catch (batchError) { + // A prunable batch error isn't a generic failure — the row-by-row pass below + // attributes it to the specific rows. + if (!shouldPrune?.(batchError)) { + this.reportError(batchError) + } + for (const row of rows) { + try { + await insert([row]) + } catch (rowError) { + if (shouldPrune?.(rowError, row)) { + prunedIds.push(row.id) + continue + } + failedIds.push(row.id) + this.logEvent({ type: 'room', name: 'failed_persist_comments_to_db' }) + this.reportError(rowError) + } + } + return { failedIds, prunedIds } + } + } + // The drain never hard-deletes comment-lane rows — the only hard deletes are + // Postgres-side cascades (user-account deletion via /admin, file deletion, version + // restore). Clients never hard-delete either (deletion is the isDeleted flag), so + // a lane-absent comment here is a stale outbox entry for a record a prune already + // removed from the lane; stamp its row soft-deleted. Idempotent, so at-least-once + // replays and already-stamped rows are no-ops. + // The stamped rows' thread ids feed the emptied-thread prune below: a thread whose + // last comment just went has no surface left and must not linger in the lane. + let deletedCommentThreadIds = new Set() + if (commentDeletes.length > 0) { + const deletedRows = await this.db + .updateTable('comment') + .set({ isDeleted: true }) + .where('id', 'in', commentDeletes) + .where('fileId', '=', fileId) + .returning('threadId') + .execute() + deletedCommentThreadIds = new Set(deletedRows.map((row) => row.threadId)) + } + const failedIds = new Set() + // A thread upsert failing its file FK can never succeed on retry — the file is gone, + // or the room's slug was never a file row at all. Prune it like an author cascade + // rather than leaving the entry to fail on every drain for the life of the room. + const threadResult = await runBatchWithFallback( + threadUpserts, + insertThreadRows, + isCommentThreadFkViolation + ) + for (const id of threadResult.failedIds) { + failedIds.add(id) + } + // A comment upsert failing the authorId FK means the author's user account was + // deleted: Postgres cascaded their comment rows away (ON DELETE CASCADE) while this + // warm room still holds the records. Retrying can never succeed, so mirror the + // cascade into the room instead: prune the records below and let their outbox + // entries clear normally (the Postgres row being absent is already the desired end + // state). Threads the prune leaves without any comments are pruned too — an + // emptied thread never renders (clients hide threads with no comments), so an + // author-cascade must not leave ghost records behind. Thread rows in + // Postgres are not touched here: comment_thread has no user FK (by design, so the + // cascade can't race the room), so the pruned thread ids are re-outboxed and a + // follow-up drain stamps the rows soft-deleted through the normal at-least-once + // acked path. + // Also prune a comment whose parent can never exist: its file is gone, or its thread + // was vetoed by the authorizer / forged by a client, so retrying is an unbounded + // loop rather than eventual consistency. + // + // A thread-FK failure alone doesn't prove that, though. Threads upsert before + // comments in this same drain, so a thread whose own upsert just failed + // transiently (timeout, serialization failure) also fails its comments' FK — and + // that thread is still queued to retry. Pruning there would delete a live comment + // moments before its parent lands, and the retry would then have nothing to attach. + // So only prune when the thread is absent from the room's lane, which is what + // "vetoed or never existed" actually looks like. `lane` is a snapshot taken at the + // top of the drain, so the later prunes below can't shift this decision. + const commentResult = await runBatchWithFallback( + commentUpserts, + insertCommentRows, + (error, row) => { + if (isCommentAuthorFkViolation(error) || isCommentFileFkViolation(error)) return true + if (!isCommentThreadIdFkViolation(error)) return false + // No row: the batch-level call, which only suppresses the duplicate report. + if (!row) return true + return !lane.has(row.threadId) + } + ) + for (const id of commentResult.failedIds) { + failedIds.add(id) + } + // Reactions after comments (comment_reaction.commentId FK). A reaction whose comment + // or reacting user has since been deleted can never insert, so it prunes like an + // author-cascaded comment rather than retrying forever. + const reactionResult = await runBatchWithFallback( + reactionUpserts, + insertReactionRows, + isCommentReactionFkViolation + ) + for (const id of reactionResult.failedIds) { + failedIds.add(id) + } + // A reaction pruned by an FK violation (its comment/thread/user was deleted and + // Postgres cascaded the row away) must also leave the warm room's SQLite, or it + // lingers as a ghost the way an author-cascaded comment would (see the comment prune + // below). Reactions have no dependent records, so this needs no thread-emptying pass. + if (reactionResult.prunedIds.length > 0) { + storage.transaction((txn) => { + for (const id of reactionResult.prunedIds) txn.delete(id as TLRecord['id']) + }) + } + // Same for a thread pruned by its file FK: the file is gone, so Postgres has already + // cascaded the thread (and its comments) away. Without this the lane keeps records + // for a file that no longer exists, which is exactly the ghost state the reaction + // and comment prunes exist to avoid. Its comments fail `comment_file_id_fkey` in + // the same drain and prune through their own path, so no dependent sweep is needed + // here. + if (threadResult.prunedIds.length > 0) { + storage.transaction((txn) => { + for (const id of threadResult.prunedIds) txn.delete(id as TLRecord['id']) + }) + } + // Un-reacting removes the record, so a lane-absent reaction is a real delete — unlike + // comments and threads, reactions have no soft-delete recovery story of their own. + if (reactionDeletes.length > 0) { + await this.db + .deleteFrom('comment_reaction') + .where('id', 'in', reactionDeletes) + .where('fileId', '=', fileId) + .execute() + } + // Lane-absent threads get the stamp treatment: stamp, never delete — a hard delete + // would FK-cascade any soft-deleted comment rows still hanging off the thread, + // destroying the recovery rows. Order vs the upserts doesn't matter here: an id + // can't be planned as both an upsert (lane-present) and a delete (lane-absent) in + // one drain. + if (threadDeletes.length > 0) { + await this.db + .updateTable('comment_thread') + .set({ isDeleted: true }) + .where('id', 'in', threadDeletes) + .where('fileId', '=', fileId) + .execute() + } + + // Reconcile @-mention rows for every comment row that made it to Postgres, so the + // notifications query can filter "comments that mention me" server-side (mentions + // live inside the body JSON, out of ZQL's reach). The write is idempotent — delete + // rows the body no longer mentions, insert the rest with ON CONFLICT DO NOTHING — + // so clock-guarded no-op replays cause no WAL churn. A mention insert failing its + // user FK (mentioned account deleted, or a bogus id) is skipped for good; any other + // failure marks the comment failed so its outbox entry stays queued and the next + // drain retries the reconcile. Soft-deleted comments keep their mention rows; the + // rows are inert because every query filters the comment itself on isDeleted. + // + // On top of the idempotency, the reconciles share one multi-comment delete and one + // multi-row insert per drain instead of paying 1-2 sequential statements per comment. + const plannedReconciles = planMentionReconciles( + commentUpserts.filter( + (row) => !failedIds.has(row.id) && !commentResult.prunedIds.includes(row.id) + ) + ) + if (plannedReconciles.length > 0) { + // What the catch below marks failed. Resolving ownership is itself fallible, so this + // has to name every planned comment, not just the ones that turn out to be ours. + const changedCommentIds = plannedReconciles.map((r) => r.commentId) + const mentionFailedIds = new Set() + try { + // Inside the try: a transient failure here fails these comments and lets the next + // drain retry them. Outside it, it would throw past the prunes, the emptied-thread + // re-outboxing and the outbox clear below, stranding a whole drain's bookkeeping + // over one record's worth of trouble. + const mentionReconciles = await this.keepOwnedMentionReconciles( + plannedReconciles, + fileId + ) + const ownedCommentIds = mentionReconciles.map((r) => r.commentId) + const desiredRows = mentionReconciles.flatMap(({ commentId, userIds }) => + userIds.map((userId) => ({ commentId, userId })) + ) + if (ownedCommentIds.length > 0) { + // One statement drops every stale row across the batch: rows belonging to a + // reconciling comment whose desired set no longer contains them. Comments whose + // set emptied contribute no desired pair, so all their rows qualify. + let deleteStale = this.db + .deleteFrom('comment_mention') + .where('commentId', 'in', ownedCommentIds) + if (desiredRows.length > 0) { + deleteStale = deleteStale.where((eb) => + eb( + eb.refTuple('commentId', 'userId'), + 'not in', + desiredRows.map((row) => eb.tuple(row.commentId, row.userId)) + ) + ) + } + await deleteStale.execute() + if (desiredRows.length > 0) { + try { + await this.db + .insertInto('comment_mention') + .values(desiredRows) + .onConflict((oc) => oc.columns(['commentId', 'userId']).doNothing()) + .execute() + } catch (batchError) { + if (!isCommentMentionFkViolation(batchError)) throw batchError + // One row's FK failure aborts the whole batch insert; retry row-by-row so + // the valid mentions land and only the FK-violating ones are skipped. + for (const row of desiredRows) { + try { + await this.db + .insertInto('comment_mention') + .values(row) + .onConflict((oc) => oc.columns(['commentId', 'userId']).doNothing()) + .execute() + } catch (rowError) { + if (!isCommentMentionFkViolation(rowError)) { + // A non-FK row failure fails only its own comment — the rest of + // the batch keeps its at-least-once progress. + mentionFailedIds.add(row.commentId) + this.reportError(rowError) + } + } + } + } + } + } + } catch (error) { + // The shared delete (or a non-FK batch-insert error) failed: every comment in + // the batch retries on the next drain, exactly as the per-comment path did. + for (const commentId of changedCommentIds) { + mentionFailedIds.add(commentId) + } + this.reportError(error) + } + for (const commentId of mentionFailedIds) { + failedIds.add(commentId) + this.logEvent({ type: 'room', name: 'failed_persist_comments_to_db' }) + } + } + + let didPruneThreads = false + if (commentResult.prunedIds.length > 0) { + this.logEvent({ type: 'room', name: 'comment_author_deleted_prune' }) + } + if (commentResult.prunedIds.length > 0 || deletedCommentThreadIds.size > 0) { + // Remove the author-cascade-pruned records from the room's storage so it stops + // carrying rows Postgres already cascaded away. Deleting through the shared + // storage handle is the sanctioned server-side mutation path: a live room + // subscribes to storage.onChange and broadcasts external transactions to its + // sessions, so connected clients see the records disappear; a closed room needs + // no broadcast, but the prune must still run — the warm DO SQLite outlives the + // room, and loadStorage short-circuits Postgres rehydration when SQLite is + // already initialized, so skipping it would keep the deleted author's comments + // alive forever. The delete does not re-enqueue outbox entries + // (onCommittedChanges only fires for client pushes), so this can't loop; a + // crash between here and the outbox clear below just replays the prune on the + // next drain (the ids are then lane-absent, taking the no-op Postgres delete + // path). + const { prunedThreadIds, prunedReactionIds } = storage.transaction((txn) => { + // Emptied-thread candidates: threads whose comments this drain stamped + // deleted, plus each cascade-pruned comment's threadId (collected, with the + // comment ids themselves, from the transaction's own reads before deleting it + // — prunedIds are comment ids, not thread ids). Never all lane threads: a + // brand-new thread whose first comment hasn't been pushed yet is also + // comment-less, and must survive. + const candidateThreadIds = new Set(deletedCommentThreadIds) + const prunedCommentIds = new Set() + for (const id of commentResult.prunedIds) { + const record = txn.get(id as TLRecord['id']) + if (record?.typeName === 'comment') { + candidateThreadIds.add(record.threadId) + prunedCommentIds.add(id) + } + txn.delete(id as TLRecord['id']) + } + // Threads left without any comments have no surface (clients hide them), so + // prune them rather than leaking a hidden record to every future session. + // The emptiness check runs on this transaction's own read surface, not the + // drain's earlier lane snapshot, so a reply committed after that snapshot + // keeps its thread alive. + const deletedThreadIds: string[] = [] + for (const threadId of findEmptiedCommentThreads(candidateThreadIds, txn)) { + // A lane-absent thread was already pruned (soft-delete or an earlier + // drain); whatever pruned it owns its Postgres row. + if (txn.get(threadId as TLRecord['id']) === undefined) continue + txn.delete(threadId as TLRecord['id']) + deletedThreadIds.push(threadId) + } + // Reactions on a pruned comment must go too, or they ghost in the warm room + // pointing at a comment that no longer exists. Unlike threads we do NOT outbox + // these or issue a Postgres delete: comment_reaction.commentId is ON DELETE + // CASCADE, so Postgres already dropped the rows when the comment row went — this + // only catches the room's SQLite up. + const deletedReactionIds = findOrphanedReactions(prunedCommentIds, txn) + for (const id of deletedReactionIds) { + txn.delete(id as TLRecord['id']) + } + return { prunedThreadIds: deletedThreadIds, prunedReactionIds: deletedReactionIds } + }).result + if (prunedReactionIds.length > 0) { + this.logEvent({ type: 'room', name: 'comment_reaction_orphan_prune' }) + } + if (prunedThreadIds.length > 0) { + this.logEvent({ type: 'room', name: 'comment_thread_emptied_prune' }) + // Outbox the pruned thread ids instead of stamping their Postgres rows + // directly: the follow-up drain (kicked below, after this drain's + // bookkeeping) sees them lane-absent and stamps them soft-deleted through + // the normal crash-safe at-least-once path. These inserts get seqs above + // this drain's bound, so the outbox clear below can't remove them. + for (const id of prunedThreadIds) { + this.ctx.storage.sql.exec('INSERT INTO comment_outbox (recordId) VALUES (?)', id) + } + didPruneThreads = true + } + } + + // Records whose soft-delete flag just reached Postgres are pruned from the room's + // lane: a thread takes its comments with it, a comment goes alone — and a thread + // left without any comments follows (see below). Postgres keeps the rows + // (recovery, and the Zero queries filter on isDeleted); the room and its clients + // drop the records for real — the warm DO SQLite outlives every reload, so without + // this prune a deleted record would keep syncing to new sessions (hidden only by + // client-side filtering) until the SQLite is lost. Lane deletes here don't + // re-enqueue outbox entries (onCommittedChanges only fires for client pushes), so + // the Postgres rows survive. A crash before this prune just leaves the flagged + // records in the lane — harmless (clients hide them) and cleaned up by the next + // drain that touches the record or the next cold load's filter. + const softDeletedThreadIds = threadUpserts + .filter((row) => row.isDeleted && !failedIds.has(row.id)) + .map((row) => row.id) + const softDeletedCommentIds = commentUpserts + .filter( + (row) => + row.isDeleted && !failedIds.has(row.id) && !commentResult.prunedIds.includes(row.id) + ) + .map((row) => row.id) + if (softDeletedThreadIds.length > 0 || softDeletedCommentIds.length > 0) { + this.logEvent({ type: 'room', name: 'comment_soft_delete_prune' }) + // Records this drain has NOT accounted for must survive the cascade sweep: a + // reply committed after this drain's bound (during the awaits above) has an + // outbox entry a later drain owns, and its row may not be in Postgres yet. + // Sweeping it from the lane would turn that entry into a no-op stamp of a row + // that was never inserted, losing the reply's recovery row. Left in the lane, + // the next drain upserts it — the stamped thread row still exists as its FK + // parent — and the cold load's thread filter keeps it out of future rooms. + const inFlightIds = new Set( + this.ctx.storage.sql + .exec('SELECT DISTINCT recordId FROM comment_outbox WHERE seq > ?', drainBound) + .toArray() + .map((row) => row.recordId as string) + ) + const { emptiedThreadIds, orphanedReactionIds } = storage.transaction((txn) => { + const prunedThreadIds = new Set() + for (const id of softDeletedThreadIds) { + if (txn.get(id as TLRecord['id']) === undefined) continue // already pruned + if (inFlightIds.has(id)) continue // updated mid-drain; the next drain re-prunes + txn.delete(id as TLRecord['id']) + prunedThreadIds.add(id) + } + // Each pruned comment's threadId (read before deleting it) is an + // emptied-thread candidate below. Candidates come only from threads that + // just lost a comment, so a brand-new thread awaiting its first comment is + // never touched — same protection as the author-cascade prune. + const candidateThreadIds = new Set() + const prunedCommentIds = new Set() + for (const id of softDeletedCommentIds) { + const record = txn.get(id as TLRecord['id']) as unknown as TLComment | undefined + if (record === undefined) continue // already pruned + if (inFlightIds.has(id)) continue // updated mid-drain; the next drain re-prunes + if (record.typeName === 'comment') { + candidateThreadIds.add(record.threadId) + } + txn.delete(id as TLRecord['id']) + prunedCommentIds.add(id) + } + if (prunedThreadIds.size > 0) { + // Materialize the id scan before deleting: comment ids are typeName-prefixed, + // so non-comment records are skipped without being read. + for (const key of [...txn.keys()]) { + if (!isCommentId(key)) continue + if (inFlightIds.has(key)) continue // committed mid-drain; a later drain owns it + const id = key as string as TLRecord['id'] + const record = txn.get(id) as unknown as TLComment | undefined + if (record?.threadId !== undefined && prunedThreadIds.has(record.threadId)) { + txn.delete(id) + prunedCommentIds.add(id) + } + } + } + // A thread whose last live comment just stamped away has no surface left and + // nobody who could ever delete it (clients hide comment-less threads, menu + // included), so prune it here rather than leaking a hidden record to every + // future session. The emptiness check runs on this transaction's read + // surface: an in-flight reply, a failed upsert, or a stamped comment + // deferred above is still in the lane and keeps its thread alive — that + // drain re-prunes and re-checks, so the cleanup converges instead of racing. + const emptied: string[] = [] + for (const threadId of findEmptiedCommentThreads(candidateThreadIds, txn)) { + // Lane-absent thread: already pruned (stamped above, or an earlier + // drain); whatever pruned it owns its Postgres row. + if (txn.get(threadId as TLRecord['id']) === undefined) continue + txn.delete(threadId as TLRecord['id']) + emptied.push(threadId) + } + // Reactions on a pruned comment leave the lane with it, or they ghost in the + // warm room pointing at a comment that's gone. Their Postgres rows stay put: + // the comment row still exists (soft-deleted, no cascade fired), the cold + // load only seeds reactions whose comment seeds, and if the comment is ever + // recovered its reactions come back with it. + const orphaned = findOrphanedReactions(prunedCommentIds, txn) + for (const id of orphaned) { + txn.delete(id as TLRecord['id']) + } + return { emptiedThreadIds: emptied, orphanedReactionIds: orphaned } + }).result + if (orphanedReactionIds.length > 0) { + this.logEvent({ type: 'room', name: 'comment_reaction_orphan_prune' }) + } + if (emptiedThreadIds.length > 0) { + this.logEvent({ type: 'room', name: 'comment_thread_emptied_prune' }) + // Re-outbox the emptied thread ids: the follow-up drain sees them lane-absent + // and stamps their rows soft-deleted through the normal crash-safe + // at-least-once path, so the rows stop re-seeding future rooms as live + // threads. These inserts get seqs above this drain's bound, so the outbox + // clear below can't remove them. + for (const id of emptiedThreadIds) { + this.ctx.storage.sql.exec('INSERT INTO comment_outbox (recordId) VALUES (?)', id) + } + didPruneThreads = true + } + } + + // Keep entries queued for any record that failed to push (they'll be retried on the + // next drain); only the entries that made it to Postgres are removed. Only entries + // within this drain's bound are touched — later entries belong to their own drain. + const toClear = outboxEntriesToClear(entries, failedIds) + if (toClear.clearAll) { + this.ctx.storage.sql.exec('DELETE FROM comment_outbox WHERE seq <= ?', drainBound) + } else { + for (const seq of toClear.seqs) { + this.ctx.storage.sql.exec('DELETE FROM comment_outbox WHERE seq = ?', seq) + } + } + + if (didPruneThreads) { + // Kick a follow-up drain for the thread ids outboxed by the prune above (their + // seqs are past this drain's bound, so this drain never touches them). Calling + // from inside the currently-running queue task is safe: ExecutionQueue.push only + // appends while a task is executing (run() early-returns on `running`), so the + // follow-up starts after this task returns — never synchronously re-entering it. + this.drainCommentOutbox() + } + }) + .catch((e) => { + this.logEvent({ type: 'room', name: 'failed_persist_comments_to_db' }) + this.reportError(e) + }) + } + + protected reportError(e: unknown, extras?: Record) { + try { + // eslint-disable-next-line @typescript-eslint/no-deprecated + this.sentry?.withScope((scope) => { + scope.setExtra('slug', this._documentInfo?.slug) + if (extras) scope.setExtras(extras) + // eslint-disable-next-line @typescript-eslint/no-deprecated + this.sentry?.captureException(e) + }) + } catch (_e) { + // Callers report from cleanup paths and from outside their own try blocks, so reporting + // must never be the thing that throws and skip the cleanup it was added to protect. + } + console.error(e) + } + + async appFileRecordCreated(file: TlaFile) { + // Seed the cache for a cold DO but never clobber a fresher row, and never null it on + // failure: loadFromDatabase's createSource check reads it across an async gap, and a + // mid-flight null would seed a from-source duplicate as an empty room. Retries stay + // honest without a cache reset because getRoom() never caches a rejection. + if (!this._fileRecordCache) this._fileRecordCache = file + + if (!this._documentInfo) { + this.setDocumentInfo({ slug: file.id, isApp: true, deleted: false }) + } + try { + await this.reportIfEffectStalls(this.getRoom(), file, 'insert') + } catch (e) { + if (shouldSkipMissingRoomEffect(e, file)) { + console.error('appFileRecordCreated: room not found for deleted file, skipping', e) + return + } + throw e + } + } + + async appFileRecordDidUpdate(file: TlaFile) { + if (!file) { + console.error('file record updated but no file found') + return + } + this._fileRecordCache = file + if (!this._documentInfo) { + this.setDocumentInfo({ slug: file.id, isApp: true, deleted: false }) + } + + try { + await this.reportIfEffectStalls(this.updateRoomForFileRecord(file), file, 'update') + } catch (e) { + if (shouldSkipMissingRoomEffect(e, file)) { + console.error('appFileRecordDidUpdate: room not found for deleted file, skipping', e) + return + } + throw e + } + } + + private async updateRoomForFileRecord(file: TlaFile) { + const storage = await this.getStorage() + // if the app file record updated, it might mean that the file name changed + storage.transaction((txn) => { + const documentRecord = txn.get(TLDOCUMENT_ID) as TLDocument + if (documentRecord.name !== file.name) { + txn.set(TLDOCUMENT_ID, { ...documentRecord, name: file.name }) + } + }) + + const room = await this.getRoom() + + // if the app file record updated, it might mean that the sharing state was updated + // in which case we should kick people out or change their permissions + // + // Only `edit` links grant guests canvas write; anything else fails closed. A tier change + // flips `isReadonly`, and the reconnect refreshes the session state the comment + // authorizers gate on — so comparing the readonly lane alone covers both. + const roomIsReadOnlyForGuests = file.shared && file.sharedLinkType !== 'edit' + + for (const session of room.getSessions()) { + if (file.isDeleted) { + room.closeSession(session.sessionId, TLSyncErrorCloseEventReason.NOT_FOUND) + continue + } + + const canAccessFiles = async () => { + const role = await getRole(this.db, session.meta.userId, file.owningGroupId) + return can(role, 'accessFiles') + } + + if (!file.shared) { + if (!(await canAccessFiles())) { + room.closeSession(session.sessionId, TLSyncErrorCloseEventReason.FORBIDDEN) + } + } else if (session.isReadonly !== roomIsReadOnlyForGuests) { + if (!(await canAccessFiles())) { + // not passing a reason means they will try to reconnect + room.closeSession(session.sessionId) + } + } + } + } + + async appFileRecordDidDelete({ id, publishedSlug }: Pick) { + if (this._documentInfo?.deleted) return + + this._fileRecordCache = null + + // prevent new connections while we clean everything up. Fall back to the argument for the + // slug (an app file's slug is its id): a never-initialized room has no documentInfo, and + // delete must stay terminal for any DO state instead of tripping the asserting getter. + this.setDocumentInfo({ slug: this._documentInfo?.slug ?? id, isApp: true, deleted: true }) + + await this.executionQueue.push(async () => { + await this.closeAllSocketsForDelete() + // setting _room to null will prevent any further persists from going through + this._room = null + this.dropBootTimings() + // The cached storage handle points at SQLite that deleteAll() drops below. + this._storage = null + // delete should be handled by the delete endpoint now + + // A row from a partially-created file can lack a publishedSlug; there are no + // published artifacts to clean up in that case. + if (publishedSlug) { + // Delete published slug mapping + await this.env.SNAPSHOT_SLUG_TO_PARENT_SLUG.delete(publishedSlug) + + // remove published files + const publishedPrefixKey = getR2KeyForRoom({ + slug: `${id}/${publishedSlug}`, + isApp: true, + }) + + await deleteAllObjectsWithPrefix(this.env.ROOM_SNAPSHOTS, publishedPrefixKey) + } + + // remove edit history + const r2Key = getR2KeyForRoom({ slug: id, isApp: true }) + // Each list page and delete batch is its own queued operation: the sweep runs both + // buckets concurrently, and unqueued beside two asset copies that is the whole + // six-connection budget. + await deleteAllVersions({ + chainBucket: this.env.ROOMS_HISTORY, + legacyBucket: this.env.ROOMS_HISTORY_EPHEMERAL, + roomKey: r2Key, + schedule: (op) => this.addR2Operation('version_chain_delete', op), + }) + + // remove main file + await this.env.ROOMS.delete(r2Key) + + // The board's thumbnails go with it. Both keys, because they are kept for different reasons + // and neither reason survives a hard delete: the file-keyed image is deliberately *not* + // deleted when a board is unshared (it stays useful behind auth, and resharing makes it an + // immediate hit), and the published-slug one only goes when the board is unpublished. Nothing + // else would ever remove either — `og/…` keys carry no version, so each board owns exactly one + // object, in a bucket with no lifecycle rule to sweep it. The render token record is dropped + // for the same reason. MCP screenshots need no equivalent: their keys carry a content version + // and their bucket has an expiration rule. + await deleteBoardThumbnails(this.env, { fileId: id, publishedSlug }) + + // finally clear storage so we don't keep the data around. deleteAll() leaves alarms + // alone, and a pending og-render alarm would fire on an object whose documentInfo it + // just erased. + await this.ctx.storage.deleteAlarm() + await this.ctx.storage.deleteAll() + }) + } + + /** + * @internal + * Best-effort by default: a failed persist here just means callers fall back to the + * last-persisted snapshot, as before. Pass `{ throwOnFailure: true }` only for callers that + * must not act on stale data (e.g. publishSnapshot reads the R2 blob straight after this + * resolves, so a swallowed failure there would publish a stale snapshot). + */ + async awaitPersist(opts?: { throwOnFailure?: boolean }) { + if (!this._documentInfo) return + await this.persistToDatabase(opts?.throwOnFailure ? { throwOnFailure: true } : undefined) + } + + // Report-only watchdog for outbox effect RPCs: fires just under the drain's 30s effect + // timeout so Sentry gets the cause before the drain bumps the row as a bare timeout. + // The outbox owns retry semantics — this never rejects or cancels the work. + private async reportIfEffectStalls( + work: Promise, + file: TlaFile, + command: 'insert' | 'update' + ): Promise { + const timer = setTimeout(() => { + const bootStage = this._bootStage + this.reportError( + new FileEffectStallError( + file.id, + command, + bootStage?.stage ?? null, + bootStage ? Date.now() - bootStage.startedAt : null, + EFFECT_STALL_REPORT_MS + ) + ) + }, EFFECT_STALL_REPORT_MS) + try { + return await work + } finally { + clearTimeout(timer) + } + } + + /** + * The MCP server's cluster index cache (mcpClusterIndexStorage.ts), which lives here because it is + * content derived from the room this object owns: it dies with the file, and needs no expiry. + * + * Storage-only — none of these boot the room. They run on a Worker's critical path, and the point + * of the cache is to be cheaper than the browser render it replaces. + */ + // `CREATE TABLE IF NOT EXISTS` is a write, and a cheap one, but running it per call would make the + // read path a write path — which is what lets a request in flight during a hard delete put storage + // back into an object that has already been emptied. Once per instance, after the delete check. + private mcpClusterIndexReady = false + private ensureMcpClusterIndex() { + if (this.mcpClusterIndexReady) return + ensureMcpClusterIndexTable(this.ctx.storage.sql) + this.mcpClusterIndexReady = true + } + + /** + * One page's stored cluster index, or null when nothing was stored for this content version. + * + * Refuses when documentInfo is absent or deleted rather than reading: `appFileRecordDidDelete` + * empties this object's storage, so a deleted object comes back from hibernation with null here. + * Allowing that state to initialize the table would resurrect storage nothing ever collects. + */ + async getMcpClusterIndex(key: McpClusterIndexKey): Promise { + if (!this._documentInfo || this._documentInfo.deleted) return null + this.ensureMcpClusterIndex() + return readMcpClusterIndexRow(this.ctx.storage.sql, key) + } + + /** + * Stores one page's cluster index, replacing whatever that page last had and dropping the rows for + * pages the board no longer has. + */ + async putMcpClusterIndex( + key: McpClusterIndexKey, + payload: string, + livePageIds: string[] + ): Promise { + if (!this._documentInfo || this._documentInfo.deleted) return + this.ensureMcpClusterIndex() + pruneMcpClusterIndexRows(this.ctx.storage.sql, key.kind, livePageIds) + writeMcpClusterIndexRow(this.ctx.storage.sql, key, payload) + } + + /** + * Reports this object's stored identity and liveness without booting the room. Reads raw + * storage so rooms with a stale documentInfo version still resolve; null for a + * never-initialized object. `connectedSockets` counts hibernation-API sockets, so it is + * accurate even while the room itself is not loaded. + */ + async __admin__getDocumentInfo() { + const info = (await this.storage.get('documentInfo')) as DocumentInfo | null + if (!info) return null + return { + slug: info.slug, + isApp: !!info.isApp, + deleted: !!info.deleted, + connectedSockets: this.ctx.getWebSockets().length, + roomLoaded: this._room !== null, + bootStage: this._bootStage?.stage ?? null, + bootStageAgeMs: this._bootStage ? Date.now() - this._bootStage.startedAt : null, + } + } + + /** + * Force-closes every connected session with CLIENT_TOO_OLD, which shipped clients treat as + * terminal: they stop reconnecting and show a "please reload" screen. This is the admin drain + * for rooms held awake around the clock by parked background tabs reconnect-looping on stale + * bundles — clients that predate the background-tab fix can only be stopped from the server. + * + * Boots the room (a one-time cost per drain), because rejection must run the protocol-aware + * path: legacy-protocol clients need an incompatibility_error message rather than a close + * code, and a hibernated session must be resumed into the room first so presence removal is + * broadcast and the leave is logged. + */ + async __admin__closeAllSessions() { + const sockets = this.ctx.getWebSockets() + if (sockets.length === 0) return { closedSockets: 0 } + + // A room that can't boot (a hard-deleted legacy room with tabs still attached, a transient + // storage error) must not abort the drain — the raw close below suffices on its own for + // every non-legacy-protocol client. + let room: TLSocketRoom | null = null + if (this._documentInfo) { + try { + room = await this.getRoom() + } catch (e) { + this.log.debug('closeAllSessions: room failed to boot, falling back to raw closes', e) + } + } + this.closeSockets(sockets, room, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD) + return { closedSockets: sockets.length } + } + + /** + * Reject every socket the object holds with `reason`. Sockets the room never saw (mid-handshake, + * room not booted) get a raw close with the same terminal code; closing twice is a no-op. + */ + private closeSockets( + sockets: WebSocket[], + room: TLSocketRoom | null, + reason: TLSyncErrorCloseEventReason + ) { + for (const ws of sockets) { + const attachment = this.getSocketAttachment(ws) + const sessionId = attachment?.sessionId + // If the DO was hibernating, this session was never re-added to the room; resume it + // so closeSession can reject it with the session's negotiated protocol. + if (room && sessionId && attachment.snapshot && !room.getSessionSnapshot(sessionId)) { + room.handleSocketResume({ + sessionId, + socket: ws, + snapshot: attachment.snapshot, + meta: attachment.meta, + }) + } + // Drop the stored snapshot before closing: the close handshake queues a + // webSocketClose event, and handleWebSocketEnd would otherwise resume the + // just-rejected session into the room again (duplicate leave, possible room re-boot). + if (attachment?.snapshot) { + ws.serializeAttachment({ ...attachment, snapshot: undefined }) + } + if (room && sessionId) { + room.closeSession(sessionId, reason) + } + try { + ws.close(TLSyncErrorCloseEventCode, reason) + } catch { + // an already-closed socket is fine + } + } + } + + // Uses the room only if it is already loaded: booting one here would read storage the delete + // is about to wipe. + private async closeAllSocketsForDelete() { + const room = this._room ? await this._room.catch(() => null) : null + this.closeSockets(this.ctx.getWebSockets(), room, TLSyncErrorCloseEventReason.NOT_FOUND) + room?.close() + } + + async __admin__hardDeleteIfLegacy() { + if (!this._documentInfo || this.documentInfo.deleted || this.documentInfo.isApp) return false + this.setDocumentInfo({ slug: this.documentInfo.slug, isApp: false, deleted: true }) + // Queued so an in-flight persist finishes before the R2 deletes rather than re-uploading + // the snapshot after them. + await this.executionQueue.push(async () => { + await this.closeAllSocketsForDelete() + // Without this the closing sessions' last-out persist re-uploads the snapshot to the keys + // deleted below. + this._room = null + this.dropBootTimings() + const slug = this.documentInfo.slug + const roomKey = getR2KeyForRoom({ slug, isApp: false }) + + // remove edit history + await deleteAllVersions({ + chainBucket: this.env.ROOMS_HISTORY, + legacyBucket: this.env.ROOMS_HISTORY_EPHEMERAL, + roomKey, + schedule: (op) => this.addR2Operation('version_chain_delete', op), + }) + + // remove main file + await this.env.ROOMS.delete(roomKey) + }) + + return true + } + + async __admin__createLegacyRoom(id: string) { + this.setDocumentInfo({ slug: id, isApp: false, deleted: false }) + const key = getR2KeyForRoom({ slug: id, isApp: false }) + await this.r2.rooms.put(key, JSON.stringify(DEFAULT_INITIAL_SNAPSHOT)) + await this.getRoom() + } +} + +const PERSIST_RETRIES_NOTIFY_THRESHOLD = 10 +const PERSIST_RETRIES_MAX = 100 +// ~10 attempts * 2s = ~20s of own retries, sized for the 30s outbox effect timeout (see +// persistToDatabase for why this is not a hard bound). +const PERSIST_RETRIES_MAX_THROWING = 10 +// Both sized against the outbox drain's 30s EFFECT_TIMEOUT_MS: the persist wait must leave +// room for the rest of the from-source boot, and the stall report must land before the +// drain gives up on the attempt. +const SOURCE_PERSIST_WAIT_TIMEOUT_MS = 10_000 +const EFFECT_STALL_REPORT_MS = 25_000 diff --git a/internal/grafana/resources/dashboards.v2.dashboard.grafana.app/ni5k8zc.yaml b/internal/grafana/resources/dashboards.v2.dashboard.grafana.app/ni5k8zc.yaml new file mode 100644 index 0000000..ea49814 --- /dev/null +++ b/internal/grafana/resources/dashboards.v2.dashboard.grafana.app/ni5k8zc.yaml @@ -0,0 +1,4883 @@ +apiVersion: dashboard.grafana.app/v2 +kind: Dashboard +metadata: + annotations: + grafana.app/folder: dotcom + grafana.app/message: MCP metrics + grafana.app/saved-from-ui: Grafana Cloud + labels: {} + name: ni5k8zc + namespace: stacks-890472 +spec: + annotations: + - kind: AnnotationQuery + spec: + builtIn: true + enable: true + hide: true + iconColor: rgba(0, 211, 255, 1) + name: Annotations & Alerts + query: + datasource: + name: -- Grafana -- + group: grafana + kind: DataQuery + spec: {} + version: v0 + cursorSync: Crosshair + editable: true + elements: + panel-2: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT count() as rooms_started + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'room_start' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT count() as rooms_started + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'room_start' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: room_start events in the selected time range. + id: 2 + links: [] + title: Rooms started + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: short + overrides: [] + options: + colorMode: value + graphMode: none + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-5: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT count() as failures + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'fail_persist' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT count() as failures + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'fail_persist' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: fail_persist events (persist gave up after retries). Anything + above zero deserves a look. + id: 5 + links: [] + title: Persist failures + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + thresholds: + mode: absolute + steps: + - color: green + value: 0 + - color: red + value: 1 + unit: short + overrides: [] + options: + colorMode: value + graphMode: none + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-7: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as event, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('room_create', 'room_reopen', 'enter', 'leave', + 'last_out') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, event + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as event, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('room_create', 'room_reopen', 'enter', 'leave', + 'last_out') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, event + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Connection lifecycle from TLFileDurableObject: creates, reopens, + enters, leaves.' + id: 7 + links: [] + title: Room lifecycle events + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-8: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as event, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('room_start', 'room_empty', 'failed_load_from_db', + 'failed_persist_to_db', 'fail_persist') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, event + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as event, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('room_start', 'room_empty', 'failed_load_from_db', + 'failed_persist_to_db', 'fail_persist') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, event + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Room starts/empties plus every failure path: failed loads, failed + persist attempts, and persists that gave up.' + id: 8 + links: [] + title: Room health & persistence events + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-9: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, count() as messages + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'send_message' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, count() as messages + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'send_message' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: send_message events from room durable objects (one per outgoing + sync message). + id: 9 + links: [] + title: Sync messages sent + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 25 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: false + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-10: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob4 as message_type, sum(double1) as bytes + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'send_message' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, message_type + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob4 as message_type, sum(double1) as bytes + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'send_message' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, message_type + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Outgoing message payload volume, split by message type (blob4). + double1 is the message length. + id: 10 + links: [] + title: Sync message bytes by type + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 30 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: bytes + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-11: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT blob4 as message_type, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'send_message' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY message_type + + ORDER BY total DESC" + rawQuery: "SELECT blob4 as message_type, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'send_message' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY message_type + + ORDER BY total DESC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Share of outgoing sync messages by type over the selected range. + id: 11 + links: [] + title: Message type mix + vizConfig: + group: piechart + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + fixedColor: '#73BF69' + mode: palette-classic + custom: + hideFrom: + legend: false + tooltip: false + viz: false + unit: short + overrides: [] + options: + displayLabels: + - percent + legend: + displayMode: list + overflow: ellipsis + placement: right + showLegend: true + values: [] + pieType: donut + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: true + sort: desc + tooltip: + hideZeros: false + mode: single + sort: none + version: 13.2.0-29854286369 + panel-12: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, quantileWeighted(0.5, double1, _sample_interval) + as p50, quantileWeighted(0.95, double1, _sample_interval) as + p95, quantileWeighted(0.99, double1, _sample_interval) as p99 + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'db_load_total' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, quantileWeighted(0.5, double1, _sample_interval) + as p50, quantileWeighted(0.95, double1, _sample_interval) as + p95, quantileWeighted(0.99, double1, _sample_interval) as p99 + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'db_load_total' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Full room snapshot load time, weighted by _sample_interval to + account for Analytics Engine sampling. + id: 12 + links: [] + title: Room load latency percentiles (db_load_total) + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-13: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as stage, avg(double1) as avg_ms + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('on_request_total', 'on_request_auth', 'on_request_rate_limit', + 'on_request_group_check', 'on_request_get_room') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, stage + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as stage, avg(double1) as avg_ms + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('on_request_total', 'on_request_auth', 'on_request_rate_limit', + 'on_request_group_check', 'on_request_get_room') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, stage + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Average duration of each stage of a room connect request (auth, + rate limit, group check, get room, total). + id: 13 + links: [] + title: Connect request latency breakdown + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-14: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as stage, avg(double1) as avg_ms + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('create_from_source_fetch_total', 'create_from_source_r2_put', + 'create_from_source_await_persist', 'create_from_source_r2_fetch', + 'db_load_r2_fetch', 'db_load_supabase_fetch', 'db_load_create_from_source', + 'db_load_comments') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, stage + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as stage, avg(double1) as avg_ms + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('create_from_source_fetch_total', 'create_from_source_r2_put', + 'create_from_source_await_persist', 'create_from_source_r2_fetch', + 'db_load_r2_fetch', 'db_load_supabase_fetch', 'db_load_create_from_source', + 'db_load_comments') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, stage + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Average duration of file duplication stages and room load sub-stages. + id: 14 + links: [] + title: Create-from-source & load stage latency + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-15: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT blob1 as operation, avg(double1) as avg_ms + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('on_request_total', 'on_request_auth', 'on_request_rate_limit', + 'on_request_group_check', 'on_request_get_room', 'get_file_record', + 'get_file_record_error', 'db_load_total', 'db_load_total_error', + 'db_load_r2_fetch', 'db_load_supabase_fetch', 'db_load_create_from_source', 'db_load_comments', + 'create_from_source_fetch_total', 'create_from_source_r2_put', + 'create_from_source_await_persist', 'create_from_source_r2_fetch') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY operation + + ORDER BY avg_ms DESC" + rawQuery: "SELECT blob1 as operation, avg(double1) as avg_ms + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('on_request_total', 'on_request_auth', 'on_request_rate_limit', + 'on_request_group_check', 'on_request_get_room', 'get_file_record', + 'get_file_record_error', 'db_load_total', 'db_load_total_error', + 'db_load_r2_fetch', 'db_load_supabase_fetch', 'db_load_create_from_source', 'db_load_comments', + 'create_from_source_fetch_total', 'create_from_source_r2_put', + 'create_from_source_await_persist', 'create_from_source_r2_fetch') + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY operation + + ORDER BY avg_ms DESC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Average duration of every timed operation in the selected range, + slowest first. + id: 15 + links: [] + title: Operation latency league table + vizConfig: + group: bargauge + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: continuous-GrYlRd + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + displayMode: gradient + legend: + calcs: [] + displayMode: list + overflow: ellipsis + placement: bottom + showLegend: false + maxVizHeight: 300 + minVizHeight: 16 + minVizWidth: 8 + namePlacement: auto + orientation: horizontal + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: true + showUnfilled: true + sizing: auto + valueMode: color + version: 13.2.0-29854286369 + panel-25: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as event, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('postgres_client_connect', 'postgres_client_end', + 'postgres_client_error') + + AND blob2 = '${environment}-tldraw-multiplayer' + + AND blob3 != 'user-do' + + GROUP BY date, event + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as event, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('postgres_client_connect', 'postgres_client_end', + 'postgres_client_error') + + AND blob2 = '${environment}-tldraw-multiplayer' + + AND blob3 != 'user-do' + + GROUP BY date, event + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Connection lifecycle from the logging pg client: connect, end, error. Excludes user-do — it dials a fresh client per checkout at roughly 20x the volume of every other pool combined, and its teardown currently mis-fires an error event per release, which would drown this panel. user-do has its own dial, acquire and lifecycle panels below.' + id: 25 + links: [] + title: Postgres client connection events (excluding user-do) + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-26: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob3 as pool, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'postgres_client_connect' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, pool + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob3 as pool, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'postgres_client_connect' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, pool + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: postgres_client_connect events split by the pool name (blob3) + that opened them — shows which routes churn connections. + id: 26 + links: [] + title: Postgres connections by pool + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 30 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-27: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob2 as origin, count() as connects + + FROM 'BEMO_ANALYTICS' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'connect' + + GROUP BY date, origin + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob2 as origin, count() as connects + + FROM 'BEMO_ANALYTICS' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'connect' + + GROUP BY date, origin + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: BEMO_ANALYTICS dataset (demo sync server). blob1 is the event + type here — there is no worker-name column and no environment filter. Requires + the BEMO_ANALYTICS table in this datasource. + id: 27 + links: [] + title: Demo server connects by origin + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-28: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as direction, count() as total + + FROM 'BEMO_ANALYTICS' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('send_message', 'receive_message') + + GROUP BY date, direction + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob1 as direction, count() as total + + FROM 'BEMO_ANALYTICS' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 IN ('send_message', 'receive_message') + + GROUP BY date, direction + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Messages sent/received by demo rooms (one data point per message). + id: 28 + links: [] + title: Demo server message throughput + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 20 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-29: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT blob2 as origin, count() as total + + FROM 'BEMO_ANALYTICS' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'connect' + + GROUP BY origin + + ORDER BY total DESC + + LIMIT 12" + rawQuery: "SELECT blob2 as origin, count() as total + + FROM 'BEMO_ANALYTICS' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'connect' + + GROUP BY origin + + ORDER BY total DESC + + LIMIT 12" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Which domains embed the demo sync server, by connect count. + id: 29 + links: [] + title: Top embedding origins + vizConfig: + group: piechart + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + fixedColor: '#73BF69' + mode: palette-classic + custom: + hideFrom: + legend: false + tooltip: false + viz: false + unit: short + overrides: [] + options: + displayLabels: + - percent + legend: + displayMode: list + overflow: ellipsis + placement: right + showLegend: true + values: [] + pieType: donut + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: true + sort: desc + tooltip: + hideZeros: false + mode: single + sort: none + version: 13.2.0-29854286369 + panel-33: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT count() as renders + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'browser_run_session' + + AND blob4 = 'mode:screenshot' + + AND blob5 = 'outcome:ok' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT count() as renders + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'browser_run_session' + + AND blob4 = 'mode:screenshot' + + AND blob5 = 'outcome:ok' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Browser sessions that produced a PNG (browser_run_session rows + with mode:screenshot and outcome:ok), across the MCP tool and the OG queue. + Measure renders, cache hits, and failed sessions are excluded. + id: 33 + links: [] + title: Board screenshots rendered + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: short + overrides: [] + options: + colorMode: value + graphMode: area + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-34: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT countIf(blob4 = 'cache:hit') / count() * 100 + as hit_rate + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob3 = 'source:og' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT countIf(blob4 = 'cache:hit') / count() * 100 + as hit_rate + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob3 = 'source:og' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Share of GET /app/social-preview requests (source:og) served + straight from the R2 cache. Stale-but-served and never-rendered responses + count against it, since both trigger a background render. + id: 34 + links: [] + title: OG image cache hit rate + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + max: 100 + min: 0 + thresholds: + mode: absolute + steps: + - color: red + value: 0 + - color: orange + value: 50 + - color: green + value: 80 + unit: percent + overrides: [] + options: + colorMode: value + graphMode: none + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-35: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT count() as failures + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob5 != 'failure:none' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT count() as failures + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob5 != 'failure:none' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Events carrying a failure reason code, across all three surfaces. + Sustained non-zero means boards are missing their previews — break it down + in the failures-by-reason panel below. + id: 35 + links: [] + title: Screenshot failures + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + thresholds: + mode: absolute + steps: + - color: green + value: 0 + - color: red + value: 1 + unit: short + overrides: [] + options: + colorMode: value + graphMode: none + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-36: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT count() as blocked + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob6 = 'rate_limit:blocked' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT count() as blocked + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob6 = 'rate_limit:blocked' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Events blocked by a rate limiter (per-IP, per-board, or the shared + global Browser Run cap). Queue jobs blocked this way are requeued rather + than dropped. + id: 36 + links: [] + title: Rate-limited screenshot events + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + thresholds: + mode: absolute + steps: + - color: green + value: 0 + - color: red + value: 1 + unit: short + overrides: [] + options: + colorMode: value + graphMode: none + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-37: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob3, 8) as surface, count() as + total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, surface + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob3, 8) as surface, count() as + total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, surface + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'All screenshot telemetry split by the surface that emitted it: + mcp (the shared board screenshot tool), og (the social-preview route), and + queue (the background render consumer).' + id: 37 + links: [] + title: Screenshot events by surface + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-38: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob3, 8) as source, countIf(blob4 + = 'cache:hit') / count() * 100 as hit_rate + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob4 != 'cache:none' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, source + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob3, 8) as source, countIf(blob4 + = 'cache:hit') / count() * 100 as hit_rate + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob4 != 'cache:none' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, source + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Share of cache-consulting requests served a fresh image, per + source. og is the one that matters most: a miss there sends a crawler the + default image and degrades the link unfurl, while an mcp miss just pays for + a render. Stale counts against it; cache:none rows (refusals, and the info + tools which have no cache) are excluded so refusal volume cannot move this + number.' + id: 38 + links: [] + title: Cache hit rate by source + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 10 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 2 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + max: 100 + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: percent + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-39: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob5, 9) as reason, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob5 != 'failure:none' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, reason + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob5, 9) as reason, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob5 != 'failure:none' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, reason + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Bounded failure reason codes: board_not_viewable, board_empty, + not_found, not_rendered_yet, no_pages, page_out_of_range, invalid_input, + and the rate_limited_* codes.' + id: 39 + links: [] + title: Screenshot failures by reason + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: + - __systemRef: hideSeriesFrom + matcher: + id: byNames + options: + mode: exclude + names: + - not_rendered_yet + prefix: 'All except:' + readOnly: true + properties: + - id: custom.hideFrom + value: + legend: false + tooltip: true + viz: true + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-40: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob4, 6) as mode, quantileWeighted(0.5, + double3, _sample_interval) as p50, quantileWeighted(0.95, double3, + _sample_interval) as p95, quantileWeighted(0.99, double3, _sample_interval) + as p99 + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'browser_run_session' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, mode + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob4, 6) as mode, quantileWeighted(0.5, + double3, _sample_interval) as p50, quantileWeighted(0.95, double3, + _sample_interval) as p95, quantileWeighted(0.99, double3, _sample_interval) + as p99 + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'browser_run_session' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, mode + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Wall-clock per browser session from the browser_run_session + ledger, split by what the session was for (measure vs screenshot) and + weighted by _sample_interval to account for Analytics Engine sampling. + Failed sessions are included — they held a browser too. + id: 40 + links: [] + title: Browser Run render duration percentiles + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-41: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob3, 8) as surface, count() as + total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob6 = 'rate_limit:blocked' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, surface + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, substring(blob3, 8) as surface, count() as + total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'mcp_shared_board_screenshot' + + AND blob6 = 'rate_limit:blocked' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, surface + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Which surface is hitting a limiter. Sustained queue blocking + means the shared global Browser Run cap is saturated; mcp blocking is usually + a single account hitting its per-account limit. + id: 41 + links: [] + title: Rate-limited screenshots by surface + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-42: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, quantileWeighted(0.5, double1, _sample_interval) as p50, quantileWeighted(0.95, double1, _sample_interval) as p95, quantileWeighted(0.99, double1, _sample_interval) as p99 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_client_connect_done' AND blob3 = 'user-do' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, quantileWeighted(0.5, double1, _sample_interval) as p50, quantileWeighted(0.95, double1, _sample_interval) as p95, quantileWeighted(0.99, double1, _sample_interval) as p99 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_client_connect_done' AND blob3 = 'user-do' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Time to establish a postgres connection for the user-do pool — TCP, TLS and auth through pgbouncer — from postgres_client_connect_done (double1), weighted by _sample_interval. user-do dials a fresh client per DO checkout and carries ~95% of dial volume, so it gets the full percentile treatment; every other pool is on the dial p95 by pool panel. A dial that hangs never completes and only shows up on the never-completed panel.' + id: 42 + links: [] + title: Postgres dial duration percentiles (user-do) + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-43: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob3 as pool, sum(if(blob1 = 'postgres_client_connect', _sample_interval, -_sample_interval)) as pending FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 IN ('postgres_client_connect', 'postgres_client_connect_done') AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, pool ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob3 as pool, sum(if(blob1 = 'postgres_client_connect', _sample_interval, -_sample_interval)) as pending FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 IN ('postgres_client_connect', 'postgres_client_connect_done') AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, pool ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: "Dial starts minus completions per pool, weighted by _sample_interval because the dataset is sampled. postgres_client_connect fires when a dial starts and postgres_client_connect_done when it resolves either way, so a healthy pool oscillates around zero and a persistently positive line counts dials that hung. Absolute dial volume is on the connections-by-pool panel. Ignore ranges before 2026-08-11 12:40 UTC — the done event didn't exist yet, so everything before then reads as a gap." + id: 43 + links: [] + title: Postgres dials never completed by pool + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: short + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-44: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, quantileWeighted(0.5, double1, _sample_interval) as p50, quantileWeighted(0.95, double1, _sample_interval) as p95, quantileWeighted(0.99, double1, _sample_interval) as p99 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_pool_acquire' AND blob3 = 'user-do' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, quantileWeighted(0.5, double1, _sample_interval) as p50, quantileWeighted(0.95, double1, _sample_interval) as p95, quantileWeighted(0.99, double1, _sample_interval) as p99 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_pool_acquire' AND blob3 = 'user-do' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Time from asking the user-do pool for a client to holding one, from postgres_pool_acquire (double1), weighted by _sample_interval. user-do serializes checkouts, so this is lock wait plus a fresh dial — a slow holder shows up here even when the dial itself is fast. Other pools are on the acquire p95 by pool panel.' + id: 44 + links: [] + title: Postgres pool acquire wait percentiles (user-do) + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-45: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob9 as followup, count() as total FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'mcp_shared_board_screenshot' AND blob3 = 'source:queue' AND blob9 != '' AND blob4 = 'cache:miss' AND blob5 = 'failure:none' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, followup ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob9 as followup, count() as total FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'mcp_shared_board_screenshot' AND blob3 = 'source:queue' AND blob9 != '' AND blob4 = 'cache:miss' AND blob5 = 'failure:none' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, followup ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Queue renders that spent Browser Run (a cache miss that then rendered without failing; durations moved to browser_run_session, so double3 is now a -1 sentinel) split by blob9. followup:false is the render a trigger asked for; followup:true is the extra render a completed job enqueues because the board moved during its capture. Request surfaces record followup:none and are excluded by the source filter. Data starts 2026-08-11 — earlier queue renders predate the followup blob and are filtered out.' + id: 45 + links: [] + title: Queue renders by follow-up state + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-46: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob3 as pool, quantileWeighted(0.95, double1, _sample_interval) as p95 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_client_connect_done' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, pool ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob3 as pool, quantileWeighted(0.95, double1, _sample_interval) as p95 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_client_connect_done' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, pool ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'p95 dial time split by pool (blob3), weighted by _sample_interval. Aggregate percentiles would just track user-do (~95% of dial volume), so every pool gets a line of its own here. Low-traffic pools are spiky — read trends, not single buckets.' + id: 46 + links: [] + title: Postgres dial p95 by pool + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-47: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob3 as pool, quantileWeighted(0.95, double1, _sample_interval) as p95 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_pool_acquire' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, pool ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob3 as pool, quantileWeighted(0.95, double1, _sample_interval) as p95 FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 = 'postgres_pool_acquire' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, pool ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'p95 acquire wait split by pool (blob3), weighted by _sample_interval. For the pg pools this is queueing while every client is busy plus any fresh dial; for user-do it is lock wait plus dial, since checkouts serialize.' + id: 47 + links: [] + title: Postgres pool acquire p95 by pool + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 12 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + min: 0 + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-48: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob1 as event, count() as total FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 IN ('postgres_client_connect', 'postgres_client_end', 'postgres_client_error') AND blob3 = 'user-do' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, event ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, blob1 as event, count() as total FROM 'MEASURE' WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) AND blob1 IN ('postgres_client_connect', 'postgres_client_end', 'postgres_client_error') AND blob3 = 'user-do' AND blob2 = '${environment}-tldraw-multiplayer' GROUP BY date, event ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Connection lifecycle for the user-do pool: connect, end, error. Split from the shared events panel because user-do dials a fresh client per DO checkout at roughly 20x the volume of every other pool combined. Known artifact: teardown currently mis-fires an error event per client release, so the error series runs at end volume — read spikes above that baseline, not the baseline itself.' + id: 48 + links: [] + title: Postgres client connection events (user-do) + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-49: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob3 as wrote, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_write' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, wrote + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob3 as wrote, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_write' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, wrote + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: version_chain_write events split keyframe vs delta. A healthy + chain writes mostly deltas with an occasional keyframe. + id: 49 + links: [] + title: Version chain writes + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-50: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob4 as reason, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_write' + + AND blob3 = 'keyframe' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, reason + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob4 as reason, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_write' + + AND blob3 = 'keyframe' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, reason + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: "Why keyframes were cut. 'segment-lost' is the lost-open-segment + recovery and 'fingerprint-mismatch' a dead-incarnation seed divergence — + spikes in either deserve a look." + id: 50 + links: [] + title: Keyframe reasons + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-51: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, if(blob3 = 'ok', 'ok', concat(blob3, ':', blob4)) + as result, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_verify' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, result + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, if(blob3 = 'ok', 'ok', concat(blob3, ':', blob4)) + as result, count() as total + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_verify' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, result + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Retired-chain verification. Fail reasons: missing | legacy-fallback + | head-mismatch | error. Any fail means a reconstruction did not reproduce + the persisted state.' + id: 51 + links: [] + title: Verify results + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: bars + fillOpacity: 80 + gradientMode: none + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 0 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-52: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: "SELECT count() as errors + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_error' + + AND blob2 = '${environment}-tldraw-multiplayer'" + rawQuery: "SELECT count() as errors + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_error' + + AND blob2 = '${environment}-tldraw-multiplayer'" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Chain writes that failed and fell back to the legacy copy. Anything + above zero deserves a look. + id: 52 + links: [] + title: Chain write errors + vizConfig: + group: stat + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: thresholds + thresholds: + mode: absolute + steps: + - color: green + value: 0 + - color: red + value: 1 + unit: short + overrides: [] + options: + colorMode: value + graphMode: none + justifyMode: auto + orientation: auto + percentChangeColorMode: standard + reduceOptions: + calcs: + - lastNotNull + fields: '' + values: false + showPercentChange: false + textMode: auto + wideLayout: true + version: 13.2.0-29854286369 + panel-53: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob3 as wrote, sum(double1) as bytes + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_write' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, wrote + + ORDER BY date ASC" + rawQuery: "SELECT toStartOfInterval(timestamp, INTERVAL '$interval' + SECOND) as date, blob3 as wrote, sum(double1) as bytes + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob1 = 'version_chain_write' + + AND blob2 = '${environment}-tldraw-multiplayer' + + GROUP BY date, wrote + + ORDER BY date ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Encoded bytes uploaded to the chain bucket, split by write type + (double1 is the encoded body size). + id: 53 + links: [] + title: Chain bytes written + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 30 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: normal + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: bytes + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-54: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: time_series + formatAs: time_series + intervalFactor: 1 + query: |- + SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, quantileWeighted(0.5, double3, _sample_interval) as total_p50, quantileWeighted(0.5, double4, _sample_interval) as browser_p50, quantileWeighted(0.95, double3, _sample_interval) as total_p95, quantileWeighted(0.95, double4, _sample_interval) as browser_p95 + FROM 'MEASURE' + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + AND blob1 = 'browser_run_session' + AND blob4 = 'mode:screenshot' + AND blob5 = 'outcome:ok' + AND double4 > 0 + AND blob2 = '${environment}-tldraw-multiplayer' + GROUP BY date + ORDER BY date ASC + rawQuery: |- + SELECT toStartOfInterval(timestamp, INTERVAL '$interval' SECOND) as date, quantileWeighted(0.5, double3, _sample_interval) as total_p50, quantileWeighted(0.5, double4, _sample_interval) as browser_p50, quantileWeighted(0.95, double3, _sample_interval) as total_p95, quantileWeighted(0.95, double4, _sample_interval) as browser_p95 + FROM 'MEASURE' + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + AND blob1 = 'browser_run_session' + AND blob4 = 'mode:screenshot' + AND blob5 = 'outcome:ok' + AND double4 > 0 + AND blob2 = '${environment}-tldraw-multiplayer' + GROUP BY date + ORDER BY date ASC + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: 'Total session wall clock (double3) against the browser wall clock Browser Run bills for it (double4, from the X-Browser-Ms-Used header), for successful screenshot renders on every surface — OG image thumbnails and MCP cluster screenshots alike. The gap between the lines is everything outside the browser — RPC, Browser Run queueing, response transfer — which is where the pipeline variance lives. Sessions with no billed time recorded (failures, the local dev service, rows from before the field existed) are excluded by double4 > 0.' + id: 54 + links: [] + title: In-browser vs total render time + vizConfig: + group: timeseries + kind: VizConfig + spec: + fieldConfig: + defaults: + color: + mode: palette-classic + custom: + axisBorderShow: false + axisCenteredZero: false + axisColorMode: text + axisLabel: '' + axisPlacement: auto + barAlignment: 0 + barWidthFactor: 0.8 + drawStyle: line + fillOpacity: 10 + gradientMode: opacity + hideFrom: + legend: false + tooltip: false + viz: false + insertNulls: false + lineInterpolation: linear + lineWidth: 1.5 + pointSize: 5 + scaleDistribution: + type: linear + showPoints: never + showValues: false + spanNulls: false + stacking: + group: A + mode: none + thresholdsStyle: + mode: 'off' + thresholds: + mode: absolute + steps: + - color: green + value: 0 + unit: ms + overrides: [] + options: + annotations: + clustering: -1 + multiLane: false + legend: + calcs: [] + displayMode: list + enableFacetedFilter: false + overflow: ellipsis + placement: bottom + showLegend: true + tooltip: + hideZeros: false + maxHeight: 600 + mode: multi + sort: desc + version: 13.2.0-29854286369 + panel-55: + kind: Panel + spec: + data: + kind: QueryGroup + spec: + queries: + - kind: PanelQuery + spec: + hidden: false + query: + datasource: + name: bdgu7tk03irr4e + group: vertamedia-clickhouse-datasource + kind: DataQuery + spec: + dateTimeType: DATETIME + editorMode: code + extrapolate: true + format: table + formatAs: table + intervalFactor: 1 + query: + "SELECT timestamp, blob1 AS operation, double1 AS ms, blob3 AS blob3, blob4 AS blob4 + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob2 = '${environment}-tldraw-multiplayer' + + AND '${load_id}' != '' AND (blob3 = '${load_id}' OR blob4 = '${load_id}') + + ORDER BY timestamp ASC" + rawQuery: + "SELECT timestamp, blob1 AS operation, double1 AS ms, blob3 AS blob3, blob4 AS blob4 + + FROM 'MEASURE' + + WHERE timestamp >= toDateTime($from) AND timestamp <= toDateTime($to) + + AND blob2 = '${environment}-tldraw-multiplayer' + + AND '${load_id}' != '' AND (blob3 = '${load_id}' OR blob4 = '${load_id}') + + ORDER BY timestamp ASC" + round: 0s + showFormattedSQL: false + showHelp: false + skip_comments: true + step: '' + version: v0 + refId: A + queryOptions: {} + transformations: [] + description: Every timer the sync worker wrote for one page load. Paste the load + id from the browser console ([first-load] total ...) into the load_id + variable. Boot stages (db_load_*, room_start) appear only when that load booted + the room; init_user carries the id in blob4 and its outcome in blob3. + id: 55 + links: [] + title: One load ($load_id) + vizConfig: + group: table + kind: VizConfig + spec: + fieldConfig: + defaults: + custom: + align: auto + cellOptions: + type: auto + inspect: false + thresholds: + mode: absolute + steps: + - color: green + value: 0 + overrides: + - matcher: + id: byName + options: ms + properties: + - id: unit + value: ms + - id: custom.cellOptions + value: + mode: gradient + type: gauge + options: + cellHeight: sm + footer: + countRows: false + fields: '' + reducer: + - sum + show: false + showHeader: true + sortBy: [] + version: 13.2.0-29854286369 + layout: + kind: RowsLayout + spec: + rows: + - kind: RowsLayoutRow + spec: + collapse: false + layout: + kind: GridLayout + spec: + items: + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-2 + height: 4 + width: 12 + x: 0 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-5 + height: 4 + width: 12 + x: 12 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-7 + height: 9 + width: 12 + x: 0 + y: 4 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-8 + height: 9 + width: 12 + x: 12 + y: 4 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-9 + height: 8 + width: 8 + x: 0 + y: 13 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-10 + height: 8 + width: 8 + x: 8 + y: 13 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-11 + height: 8 + width: 8 + x: 16 + y: 13 + title: Rooms and sync + - kind: RowsLayoutRow + spec: + collapse: false + layout: + kind: GridLayout + spec: + items: + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-12 + height: 9 + width: 12 + x: 0 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-13 + height: 9 + width: 12 + x: 12 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-14 + height: 8 + width: 12 + x: 0 + y: 9 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-15 + height: 8 + width: 12 + x: 12 + y: 9 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-55 + height: 8 + width: 24 + x: 0 + y: 17 + title: Room and connect latency + - kind: RowsLayoutRow + spec: + collapse: true + layout: + kind: GridLayout + spec: + items: + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-49 + height: 8 + width: 12 + x: 0 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-50 + height: 8 + width: 12 + x: 12 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-51 + height: 8 + width: 12 + x: 0 + y: 8 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-53 + height: 8 + width: 12 + x: 12 + y: 8 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-52 + height: 4 + width: 6 + x: 0 + y: 16 + title: Version chains + - kind: RowsLayoutRow + spec: + collapse: true + layout: + kind: GridLayout + spec: + items: + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-25 + height: 8 + width: 12 + x: 0 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-26 + height: 8 + width: 12 + x: 12 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-48 + height: 8 + width: 12 + x: 0 + y: 8 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-43 + height: 8 + width: 12 + x: 12 + y: 8 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-42 + height: 8 + width: 12 + x: 0 + y: 16 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-46 + height: 8 + width: 12 + x: 12 + y: 16 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-44 + height: 8 + width: 12 + x: 0 + y: 24 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-47 + height: 8 + width: 12 + x: 12 + y: 24 + title: Postgres connections + - kind: RowsLayoutRow + spec: + collapse: true + layout: + kind: GridLayout + spec: + items: + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-33 + height: 4 + width: 6 + x: 0 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-34 + height: 4 + width: 6 + x: 6 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-35 + height: 4 + width: 6 + x: 12 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-36 + height: 4 + width: 6 + x: 18 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-37 + height: 8 + width: 8 + x: 0 + y: 4 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-38 + height: 8 + width: 8 + x: 8 + y: 4 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-39 + height: 8 + width: 8 + x: 16 + y: 4 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-40 + height: 8 + width: 12 + x: 0 + y: 12 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-41 + height: 8 + width: 12 + x: 12 + y: 12 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-45 + height: 8 + width: 24 + x: 0 + y: 20 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-54 + height: 8 + width: 12 + x: 0 + y: 28 + title: Screenshots and OG images + - kind: RowsLayoutRow + spec: + collapse: true + layout: + kind: GridLayout + spec: + items: + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-27 + height: 8 + width: 12 + x: 0 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-28 + height: 8 + width: 6 + x: 12 + y: 0 + - kind: GridLayoutItem + spec: + element: + kind: ElementReference + name: panel-29 + height: 8 + width: 6 + x: 18 + y: 0 + title: Demo server (bemo) + links: [] + liveNow: false + preload: false + tags: + - tldraw + - analytics-engine + - provisioned + - repo:tldraw/tldraw + timeSettings: + autoRefresh: 5s + autoRefreshIntervals: + - 30s + - 1m + - 5m + - 15m + - 30m + - 1h + - 2h + - 1d + fiscalYearStartMonth: 0 + from: now-3h + hideTimepicker: false + timezone: browser + to: now + title: Dotcom events + variables: + - kind: TextVariable + spec: + current: + text: production + value: production + description: Environment to filter by. Use production, staging, or your pr number + (pr-42). Applies to MEASURE panels only — the demo server panels query their + own dataset. + hide: dontHide + label: 'Environment: production | staging | pr-42' + name: environment + query: production + skipUrlSync: false + - kind: TextVariable + spec: + current: + text: '' + value: '' + description: A first_load id from the browser console, to list every sync worker + timer for that page load in the "One load" panel. + hide: dontHide + label: 'Load id (from the first_load console line)' + name: load_id + query: '' + skipUrlSync: false diff --git a/packages/editor/api-report.api.md b/packages/editor/api-report.api.md new file mode 100644 index 0000000..ef9ad52 --- /dev/null +++ b/packages/editor/api-report.api.md @@ -0,0 +1,5520 @@ +## API Report File for "@tldraw/editor" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Atom } from '@tldraw/state'; +import { AtomSet } from '@tldraw/store'; +import { Awaitable } from '@tldraw/utils'; +import { BoxModel } from '@tldraw/tlschema'; +import { ComponentType } from 'react'; +import { Computed } from '@tldraw/state'; +import { CustomRecordInfo } from '@tldraw/tlschema'; +import { Dispatch } from 'react'; +import { Editor as Editor_2 } from '@tiptap/core'; +import { EditorProviderProps as EditorProviderProps_2 } from '@tiptap/react'; +import EventEmitter from 'eventemitter3'; +import { ExoticComponent } from 'react'; +import { ExtractShapeByProps } from '@tldraw/tlschema'; +import { ForwardRefExoticComponent } from 'react'; +import { FragmentProps } from 'react'; +import { HistoryEntry } from '@tldraw/store'; +import { IndexKey } from '@tldraw/utils'; +import { JsonObject } from '@tldraw/utils'; +import { JSX } from 'react/jsx-runtime'; +import { LegacyMigrations } from '@tldraw/store'; +import { MemoExoticComponent } from 'react'; +import { MigrationSequence } from '@tldraw/store'; +import { Node as Node_2 } from '@tiptap/pm/model'; +import { PerformanceTracker } from '@tldraw/utils'; +import * as React_2 from 'react'; +import { default as React_3 } from 'react'; +import { ReactElement } from 'react'; +import { ReactNode } from 'react'; +import { ReactPortal } from 'react'; +import { RecordProps } from '@tldraw/tlschema'; +import { RecordsDiff } from '@tldraw/store'; +import { RefAttributes } from 'react'; +import { RefObject } from 'react'; +import { SerializedSchema } from '@tldraw/store'; +import { SerializedStore } from '@tldraw/store'; +import { SetStateAction } from 'react'; +import { Signal } from '@tldraw/state'; +import { Store } from '@tldraw/store'; +import { StoreSchema } from '@tldraw/store'; +import { StoreSideEffects } from '@tldraw/store'; +import { StyleProp } from '@tldraw/tlschema'; +import { StylePropValue } from '@tldraw/tlschema'; +import { T } from '@tldraw/validate'; +import { Timers } from '@tldraw/utils'; +import { TLAsset } from '@tldraw/tlschema'; +import { TLAssetId } from '@tldraw/tlschema'; +import { TLAssetPartial } from '@tldraw/tlschema'; +import { TLAssetStore } from '@tldraw/tlschema'; +import { TLBaseShape } from '@tldraw/tlschema'; +import { TLBinding } from '@tldraw/tlschema'; +import { TLBindingCreate } from '@tldraw/tlschema'; +import { TLBindingId } from '@tldraw/tlschema'; +import { TLBindingUpdate } from '@tldraw/tlschema'; +import { TLBookmarkAsset } from '@tldraw/tlschema'; +import { TLCamera } from '@tldraw/tlschema'; +import { TLCreateShapePartial } from '@tldraw/tlschema'; +import { TLCursor } from '@tldraw/tlschema'; +import { TLCursorType } from '@tldraw/tlschema'; +import { TLDefaultColor } from '@tldraw/tlschema'; +import { TLDefaultColorStyle } from '@tldraw/tlschema'; +import { TLDefaultDashStyle } from '@tldraw/tlschema'; +import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'; +import { TLDocument } from '@tldraw/tlschema'; +import { TLFontFace } from '@tldraw/tlschema'; +import { TLGroupShape } from '@tldraw/tlschema'; +import { TLHandle } from '@tldraw/tlschema'; +import { TLImageAsset } from '@tldraw/tlschema'; +import { TLInstance } from '@tldraw/tlschema'; +import { TLInstancePageState } from '@tldraw/tlschema'; +import { TLInstancePresence } from '@tldraw/tlschema'; +import { TLOpacityType } from '@tldraw/tlschema'; +import { TLPage } from '@tldraw/tlschema'; +import { TLPageId } from '@tldraw/tlschema'; +import { TLParentId } from '@tldraw/tlschema'; +import { TLPropsMigrations } from '@tldraw/tlschema'; +import { TLRecord } from '@tldraw/tlschema'; +import { TLRichText } from '@tldraw/tlschema'; +import { TLScribble } from '@tldraw/tlschema'; +import { TLShape } from '@tldraw/tlschema'; +import { TLShapeCrop } from '@tldraw/tlschema'; +import { TLShapeId } from '@tldraw/tlschema'; +import { TLShapePartial } from '@tldraw/tlschema'; +import { TLStore } from '@tldraw/tlschema'; +import { TLStoreProps } from '@tldraw/tlschema'; +import { TLStoreSchema } from '@tldraw/tlschema'; +import { TLStoreSnapshot } from '@tldraw/tlschema'; +import { TLTheme } from '@tldraw/tlschema'; +import { TLThemeColors } from '@tldraw/tlschema'; +import { TLThemeId } from '@tldraw/tlschema'; +import { TLThemes } from '@tldraw/tlschema'; +import { TLUnknownAsset } from '@tldraw/tlschema'; +import { TLUnknownBinding } from '@tldraw/tlschema'; +import { TLUnknownShape } from '@tldraw/tlschema'; +import { TLUser } from '@tldraw/tlschema'; +import { TLUserId } from '@tldraw/tlschema'; +import { TLUserStore } from '@tldraw/tlschema'; +import { TLVideoAsset } from '@tldraw/tlschema'; +import { UnknownRecord } from '@tldraw/store'; +import { VecModel } from '@tldraw/tlschema'; + +// @internal (undocumented) +export function activeElementShouldCaptureKeys(includeButtonsAndMenus?: boolean, doc?: Document): boolean; + +// @public +export function angleDistance(fromAngle: number, toAngle: number, direction: number): number; + +// @internal (undocumented) +export function applyRotationToSnapshotShapes({ delta, editor, snapshot, stage, centerOverride, }: { + centerOverride?: VecLike; + delta: number; + editor: Editor; + snapshot: TLRotationSnapshot; + stage: 'end' | 'one-off' | 'start' | 'update'; +}): void; + +// @public +export function approximately(a: number, b: number, precision?: number): boolean; + +// @public (undocumented) +export class Arc2d extends Geometry2d { + constructor(config: Omit & { + center: Vec; + end: Vec; + largeArcFlag: number; + start: Vec; + sweepFlag: number; + }); + // (undocumented) + getLength(): number; + // (undocumented) + getSvgPathData(first?: boolean): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike): boolean; + // (undocumented) + nearestPoint(point: VecLike): Vec; +} + +// @public +export function areAnglesCompatible(a: number, b: number): boolean; + +// @public +export abstract class AssetUtil { + constructor(editor: Editor); + acceptsMimeType(mimeType: string): boolean; + static configure>(this: T, options: T extends new (...args: any[]) => { + options: infer Options; + } ? Partial : never): T; + // (undocumented) + editor: Editor; + getAssetFromFile(_file: File, _assetId: TLAssetId): Promise; + abstract getDefaultProps(): Asset['props']; + getSupportedMimeTypes(): readonly string[]; + // (undocumented) + static migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations; + options: {}; + // (undocumented) + static props?: RecordProps; + static type: string; +} + +// @public (undocumented) +export function average(A: VecLike, B: VecLike): string; + +// @public (undocumented) +export abstract class BaseBoxShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + onCreate?(_shape: null | TLShape): null | void; + // (undocumented) + abstract shapeType: TLBaseBoxShape['type']; +} + +// @public (undocumented) +export abstract class BaseBoxShapeUtil extends ShapeUtil { + // (undocumented) + getGeometry(shape: Shape): Geometry2d; + // (undocumented) + getHandleSnapGeometry(shape: Shape): HandleSnapGeometry; + // (undocumented) + getInterpolatedProps(startShape: Shape, endShape: Shape, t: number): Shape['props']; + // (undocumented) + onResize(shape: any, info: TLResizeInfo): any; +} + +// @public +export abstract class BaseFrameLikeShapeUtil extends BaseBoxShapeUtil { + // (undocumented) + canReceiveNewChildrenOfType(shape: Shape, _type: TLShape['type']): boolean; + // (undocumented) + canRemoveChildrenOfType(shape: Shape, _type: TLShape['type']): boolean; + // (undocumented) + getClipPath(shape: Shape): undefined | Vec[]; + // (undocumented) + isFrameLike(_shape: Shape): boolean; + // (undocumented) + onDragShapesIn(shape: Shape, draggingShapes: TLShape[], { initialParentIds, initialIndices }: TLDragShapesInInfo): void; + // (undocumented) + onDragShapesOut(shape: Shape, draggingShapes: TLShape[], info: TLDragShapesOutInfo): void; + // (undocumented) + providesBackgroundForChildren(): boolean; + // (undocumented) + shouldClipChild(child: TLShape): boolean; +} + +// @public (undocumented) +export interface BatchMeasurementRequest { + // (undocumented) + html: string; + // (undocumented) + opts: TLMeasureTextOpts; +} + +// @public +export interface BindingOnChangeOptions { + bindingAfter: Binding; + bindingBefore: Binding; +} + +// @public +export interface BindingOnCreateOptions { + binding: Binding; +} + +// @public +export interface BindingOnDeleteOptions { + binding: Binding; +} + +// @public +export interface BindingOnShapeChangeOptions { + binding: Binding; + reason: 'ancestry' | 'self'; + shapeAfter: TLShape; + shapeBefore: TLShape; +} + +// @public +export interface BindingOnShapeDeleteOptions { + binding: Binding; + shape: TLShape; +} + +// @public +export interface BindingOnShapeIsolateOptions { + binding: Binding; + removedShape: TLShape; +} + +// @public (undocumented) +export abstract class BindingUtil { + constructor(editor: Editor); + // (undocumented) + editor: Editor; + abstract getDefaultProps(): Partial; + // (undocumented) + static migrations?: TLPropsMigrations; + onAfterChange?(options: BindingOnChangeOptions): void; + onAfterChangeFromShape?(options: BindingOnShapeChangeOptions): void; + onAfterChangeToShape?(options: BindingOnShapeChangeOptions): void; + onAfterCreate?(options: BindingOnCreateOptions): void; + onAfterDelete?(options: BindingOnDeleteOptions): void; + onBeforeChange?(options: BindingOnChangeOptions): Binding | void; + onBeforeCreate?(options: BindingOnCreateOptions): Binding | void; + onBeforeDelete?(options: BindingOnDeleteOptions): void; + onBeforeDeleteFromShape?(options: BindingOnShapeDeleteOptions): void; + onBeforeDeleteToShape?(options: BindingOnShapeDeleteOptions): void; + onBeforeIsolateFromShape?(options: BindingOnShapeIsolateOptions): void; + onBeforeIsolateToShape?(options: BindingOnShapeIsolateOptions): void; + onOperationComplete?(): void; + // (undocumented) + static props?: RecordProps; + static type: string; +} + +// @public +export interface BoundsSnapGeometry { + points?: VecModel[]; +} + +// @public (undocumented) +export interface BoundsSnapPoint { + // (undocumented) + handle?: SelectionCorner; + // (undocumented) + id: string; + // (undocumented) + x: number; + // (undocumented) + y: number; +} + +// @public (undocumented) +export class BoundsSnaps { + constructor(manager: SnapManager); + // (undocumented) + readonly editor: Editor; + // (undocumented) + getSnapPoints(shapeId: TLShapeId): BoundsSnapPoint[]; + // (undocumented) + readonly manager: SnapManager; + // (undocumented) + snapResizeShapes({ initialSelectionPageBounds, dragDelta, handle: originalHandle, isAspectRatioLocked, isResizingFromCenter, }: { + dragDelta: Vec; + handle: SelectionCorner | SelectionEdge; + initialSelectionPageBounds: Box; + isAspectRatioLocked: boolean; + isResizingFromCenter: boolean; + }): SnapData; + // (undocumented) + snapTranslateShapes({ lockedAxis, initialSelectionPageBounds, initialSelectionSnapPoints, dragDelta, }: { + dragDelta: Vec; + initialSelectionPageBounds: Box; + initialSelectionSnapPoints: BoundsSnapPoint[]; + lockedAxis: 'x' | 'y' | null; + }): SnapData; +} + +// @public (undocumented) +export class Box { + constructor(x?: number, y?: number, w?: number, h?: number); + // (undocumented) + get aspectRatio(): number; + // (undocumented) + get bottom(): number; + // (undocumented) + get center(): Vec; + set center(v: Vec); + // (undocumented) + clone(): Box; + // (undocumented) + static Collides(A: Box, B: Box): boolean; + // (undocumented) + collides(B: Box): boolean; + // (undocumented) + static Common(boxes: Box[]): Box; + // (undocumented) + static Contains(A: Box, B: Box): boolean; + // (undocumented) + contains(B: Box): boolean; + // (undocumented) + static ContainsApproximately(A: Box, B: Box, precision?: number): boolean; + // (undocumented) + static ContainsPoint(A: Box, B: VecLike, margin?: number): boolean; + // (undocumented) + containsPoint(V: VecLike, margin?: number): boolean; + // (undocumented) + get corners(): Vec[]; + // (undocumented) + get cornersAndCenter(): Vec[]; + // (undocumented) + static Equals(a: Box | BoxModel, b: Box | BoxModel): boolean; + // (undocumented) + equals(other: Box | BoxModel): boolean; + // (undocumented) + static Expand(A: Box, B: Box): Box; + // (undocumented) + expand(A: Box): this; + // (undocumented) + static ExpandBy(A: Box, n: number): Box; + // (undocumented) + expandBy(n: number): this; + // (undocumented) + static From(box: BoxModel): Box; + // (undocumented) + static FromCenter(center: VecLike, size: VecLike): Box; + // (undocumented) + static FromPoints(points: VecLike[]): Box; + // (undocumented) + getHandlePoint(handle: SelectionCorner | SelectionEdge): Vec; + // (undocumented) + h: number; + // (undocumented) + get height(): number; + set height(n: number); + // (undocumented) + static Includes(A: Box, B: Box): boolean; + // (undocumented) + includes(B: Box): boolean; + // (undocumented) + isValid(): boolean; + // (undocumented) + get left(): number; + // (undocumented) + get maxX(): number; + // (undocumented) + get maxY(): number; + // (undocumented) + get midX(): number; + // (undocumented) + get midY(): number; + // (undocumented) + get minX(): number; + set minX(n: number); + // (undocumented) + get minY(): number; + set minY(n: number); + // (undocumented) + get point(): Vec; + set point(val: Vec); + // (undocumented) + static Resize(box: Box, handle: SelectionCorner | SelectionEdge | string, dx: number, dy: number, isAspectRatioLocked?: boolean): { + box: Box; + scaleX: number; + scaleY: number; + }; + // (undocumented) + resize(handle: SelectionCorner | SelectionEdge | string, dx: number, dy: number): void; + // (undocumented) + get right(): number; + // (undocumented) + scale(n: number): this; + // (undocumented) + set(x?: number, y?: number, w?: number, h?: number): this; + // (undocumented) + setTo(B: Box): this; + // (undocumented) + static Sides(A: Box, inset?: number): Vec[][]; + // (undocumented) + get sides(): Array<[Vec, Vec]>; + // (undocumented) + get size(): Vec; + // (undocumented) + snapToGrid(size: number): void; + // (undocumented) + toFixed(): this; + // (undocumented) + toJson(): BoxModel; + // (undocumented) + get top(): number; + // (undocumented) + translate(delta: VecLike): this; + // (undocumented) + union(box: BoxModel): this; + // (undocumented) + w: number; + // (undocumented) + get width(): number; + set width(n: number); + // (undocumented) + x: number; + // (undocumented) + y: number; + // (undocumented) + static ZeroFix(other: Box | BoxModel): Box; + // (undocumented) + zeroFix(): this; +} + +// @public (undocumented) +export type BoxLike = Box | BoxModel; + +// @public (undocumented) +export function canonicalizeRotation(a: number): number; + +// @internal (undocumented) +export interface CanvasMaxSize { + // (undocumented) + maxArea: number; + // (undocumented) + maxHeight: number; + // (undocumented) + maxWidth: number; +} + +// @public +export function centerOfCircleFromThreePoints(a: VecLike, b: VecLike, c: VecLike): null | Vec; + +// @public (undocumented) +export class Circle2d extends Geometry2d { + constructor(config: Omit & { + isFilled: boolean; + radius: number; + x?: number; + y?: number; + }); + // (undocumented) + config: Omit & { + isFilled: boolean; + radius: number; + x?: number; + y?: number; + }; + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean): number; + // (undocumented) + getBounds(): Box; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, distance?: number): boolean; + // (undocumented) + hitTestPoint(point: VecLike, margin?: number, hitInside?: boolean): boolean; + // (undocumented) + nearestPoint(point: VecLike): Vec; +} + +// @public +export function clamp(n: number, min: number): number; + +// @public +export function clamp(n: number, min: number, max: number): number; + +// @public +export function clampRadians(r: number): number; + +// @internal (undocumented) +export function clampToBrowserMaxCanvasSize(width: number, height: number): [number, number]; + +// @public (undocumented) +export class ClickManager { + constructor(editor: Editor); + // @internal + cancelDoubleClickTimeout(): void; + get clickState(): TLClickState | undefined; + // (undocumented) + editor: Editor; + // (undocumented) + _getClickTimeout(state: TLClickState, id?: string): void; + // (undocumented) + handlePointerEvent(info: TLPointerEventInfo): TLClickEventInfo | TLPointerEventInfo; + // (undocumented) + lastPointerInfo: TLPointerEventInfo; +} + +// @public +export function clockwiseAngleDist(a0: number, a1: number): number; + +// @public +export class CollaboratorsManager { + constructor(editor: Editor); + getCollaborators(): TLInstancePresence[]; + getCollaboratorsOnCurrentPage(): TLInstancePresence[]; + getVisibleCollaborators(): TLInstancePresence[]; + getVisibleCollaboratorsOnCurrentPage(): TLInstancePresence[]; +} + +// @public (undocumented) +export function ContainerProvider({ container, children }: ContainerProviderProps): JSX.Element; + +// @public (undocumented) +export interface ContainerProviderProps { + // (undocumented) + children: React.ReactNode; + // (undocumented) + container: HTMLElement; +} + +// @public (undocumented) +export const coreShapes: readonly [typeof GroupShapeUtil]; + +// @public +export function counterClockwiseAngleDist(a0: number, a1: number): number; + +// @public (undocumented) +export function createDebugValue(name: string, { defaults, shouldStoreForSession, }: { + defaults: DebugFlagDefaults; + shouldStoreForSession?: boolean; +}): DebugFlag; + +// @public +export function createDeepLinkString(deepLink: TLDeepLink): string; + +// @public +export function createSessionStateSnapshotSignal(store: TLStore): Signal; + +// @public (undocumented) +export function createTLCurrentUser(opts?: { + setUserPreferences?: (userPreferences: TLUserPreferences) => void; + userPreferences?: Signal; +}): TLCurrentUser; + +// @public +export function createTLSchemaFromUtils(opts: TLStoreSchemaOptions): StoreSchema; + +// @public +export function createTLStore({ initialData, defaultName, id, assets, users, onMount, collaboration, themes, ...rest }?: TLStoreOptions): TLStore; + +// @public (undocumented) +export class CubicBezier2d extends Polyline2d { + constructor(config: Omit & { + cp1: Vec; + cp2: Vec; + end: Vec; + resolution?: number; + start: Vec; + }); + // (undocumented) + distanceToPoint(point: VecLike, _hitInside?: boolean): number; + // (undocumented) + static GetAtT(segment: CubicBezier2d, t: number): Vec; + // (undocumented) + getLength(_filters?: Geometry2dFilters, precision?: number): number; + // (undocumented) + getSvgPathData(first?: boolean): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + nearestPoint(A: VecLike): Vec; +} + +// @public (undocumented) +export class CubicSpline2d extends Geometry2d { + constructor(config: Omit & { + points: Vec[]; + }); + // (undocumented) + distanceToPoint(point: VecLike, _hitInside?: boolean): number; + // (undocumented) + getLength(): number; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike): boolean; + // (undocumented) + nearestPoint(A: VecLike): Vec; + // (undocumented) + get segments(): CubicBezier2d[]; +} + +// @public +export function dataUrlToFile(url: string, filename: string, mimeType: string): Promise; + +// @public (undocumented) +export interface DebugFlag extends DebugFlagDef, Atom { + // (undocumented) + reset(): void; +} + +// @public (undocumented) +export interface DebugFlagDef { + // (undocumented) + defaults: DebugFlagDefaults; + // (undocumented) + name: string; + // (undocumented) + shouldStoreForSession: boolean; +} + +// @public (undocumented) +export interface DebugFlagDefaults { + // (undocumented) + all: T; + // (undocumented) + development?: T; + // (undocumented) + production?: T; + // (undocumented) + staging?: T; +} + +// @internal (undocumented) +export const debugFlags: { + readonly a11y: DebugFlag; + readonly debugElbowArrows: DebugFlag; + readonly debugGeometry: DebugFlag; + readonly debugSvg: DebugFlag; + readonly editOnType: DebugFlag; + readonly forceSrgb: DebugFlag; + readonly hideShapes: DebugFlag; + readonly logElementRemoves: DebugFlag; + readonly logPointerCaptures: DebugFlag; + readonly logPreventDefaults: DebugFlag; + readonly measurePerformance: DebugFlag; + readonly showFps: DebugFlag; + readonly throwToBlob: DebugFlag; +}; + +// @internal (undocumented) +export const DEFAULT_ANIMATION_OPTIONS: { + duration: number; + easing: (t: number) => number; +}; + +// @internal (undocumented) +export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions; + +// @public +export const DEFAULT_THEME: TLTheme; + +// @public (undocumented) +export function DefaultBackground(): JSX.Element; + +// @public (undocumented) +export function DefaultCanvas({ className }: TLCanvasComponentProps): JSX.Element; + +// @public +export const DefaultCursor: MemoExoticComponent<({ className, zoom, point, color, name, chatMessage, }: TLCursorProps) => JSX.Element | null>; + +// @public (undocumented) +export function DefaultErrorFallback({ error, editor }: TLErrorFallbackProps): JSX.Element; + +// @public (undocumented) +export function DefaultGrid({ x, y, z, size }: TLGridProps): JSX.Element; + +// @public (undocumented) +export const DefaultShapeWrapper: ForwardRefExoticComponent>; + +// @public (undocumented) +export function DefaultSpinner(props: TLSpinnerProps): JSX.Element; + +// @public (undocumented) +export function DefaultSvgDefs(): null; + +// @public (undocumented) +export const defaultTldrawOptions: { + readonly actionShortcutsLocation: 'swap'; + readonly adjacentShapeMargin: 10; + readonly animationMediumMs: 320; + readonly camera: TLCameraOptions; + readonly cameraMovingTimeoutMs: 64; + readonly cameraSlideFriction: 0.09; + readonly coarseDragDistanceSquared: 36; + readonly coarseHandleRadius: 20; + readonly coarseHitTestMargin: 4; + readonly coarsePointerWidth: 12; + readonly collaboratorCheckIntervalMs: 1200; + readonly collaboratorIdleTimeoutMs: 3000; + readonly collaboratorInactiveTimeoutMs: 60000; + readonly createTextOnCanvasDoubleClick: true; + readonly debouncedZoom: true; + readonly debouncedZoomThreshold: 500; + readonly deepLinks: undefined; + readonly defaultSvgPadding: 32; + readonly doubleClickDurationMs: 450; + readonly dragDistanceSquared: 16; + readonly edgeScrollDelay: 200; + readonly edgeScrollDistance: 8; + readonly edgeScrollEaseDuration: 200; + readonly edgeScrollSpeed: 25; + readonly enableToolbarKeyboardShortcuts: true; + readonly experimental__onDropOnCanvas: undefined; + readonly exportProvider: ExoticComponent; + readonly flattenImageBoundsExpand: 64; + readonly flattenImageBoundsPadding: 16; + readonly followChaseViewportSnap: 2; + readonly gridSteps: readonly [{ + readonly mid: 0.15; + readonly min: -1; + readonly step: 64; + }, { + readonly mid: 0.375; + readonly min: 0.05; + readonly step: 16; + }, { + readonly mid: 1; + readonly min: 0.15; + readonly step: 4; + }, { + readonly mid: 2.5; + readonly min: 0.7; + readonly step: 1; + }]; + readonly handleRadius: 12; + readonly hitTestMargin: 3; + readonly laserDelayMs: 1200; + readonly laserFadeoutMs: 500; + readonly longPressDurationMs: 500; + readonly maxExportDelayMs: 5000; + readonly maxFilesAtOnce: 100; + readonly maxFontsToLoadBeforeRender: number; + readonly maxPages: 40; + readonly maxShapesPerPage: 4000; + readonly multiClickDurationMs: 200; + readonly nonce: undefined; + readonly onBeforeCopyToClipboard: undefined; + readonly onBeforePasteFromClipboard: undefined; + readonly onClipboardPasteRaw: undefined; + readonly quickZoomPreservesScreenBounds: true; + readonly rightClickPanning: true; + readonly selectLockedShapes: false; + readonly snapThreshold: 8; + readonly spacebarPanning: true; + readonly temporaryAssetPreviewLifetimeMs: 180000; + readonly text: {}; + readonly textShadowLod: 0.35; + readonly tooltipDelayMs: 700; + readonly uiCoarseDragDistanceSquared: 625; + readonly uiDragDistanceSquared: 16; + readonly zoomToFitPadding: 128; +}; + +// @public (undocumented) +export const defaultUserPreferences: Readonly<{ + animationSpeed: 0 | 1; + areKeyboardShortcutsEnabled: true; + color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B"; + colorScheme: "light"; + edgeScrollSpeed: 1; + enhancedA11yMode: false; + inputMode: null; + isDynamicSizeMode: false; + isPasteAtCursorMode: false; + isSnapMode: false; + isWrapMode: false; + isZoomDirectionInverted: false; + locale: "ar" | "bn" | "ca" | "cs" | "da" | "de" | "el" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "gu-in" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "km-kh" | "kn" | "ko-kr" | "ml" | "mr" | "ms" | "ne" | "nl" | "no" | "pa" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "so" | "sv" | "ta" | "te" | "th" | "tl" | "tr" | "uk" | "ur" | "vi" | "zh-cn" | "zh-tw"; + name: ""; +}>; + +// @public (undocumented) +export const defaultUserStore: TLUserStore; + +// @public +export function degreesToRadians(d: number): number; + +// @public (undocumented) +export const EASINGS: { + readonly easeInCubic: (t: number) => number; + readonly easeInExpo: (t: number) => number; + readonly easeInOutCubic: (t: number) => number; + readonly easeInOutExpo: (t: number) => number; + readonly easeInOutQuad: (t: number) => number; + readonly easeInOutQuart: (t: number) => number; + readonly easeInOutQuint: (t: number) => number; + readonly easeInOutSine: (t: number) => number; + readonly easeInQuad: (t: number) => number; + readonly easeInQuart: (t: number) => number; + readonly easeInQuint: (t: number) => number; + readonly easeInSine: (t: number) => number; + readonly easeOutCubic: (t: number) => number; + readonly easeOutExpo: (t: number) => number; + readonly easeOutQuad: (t: number) => number; + readonly easeOutQuart: (t: number) => number; + readonly easeOutQuint: (t: number) => number; + readonly easeOutSine: (t: number) => number; + readonly linear: (t: number) => number; +}; + +// @public (undocumented) +export class Edge2d extends Geometry2d { + constructor(config: { + end: Vec; + start: Vec; + }); + // (undocumented) + distanceToPoint(point: VecLike, _hitInside?: boolean): number; + // (undocumented) + getLength(): number; + // (undocumented) + getSvgPathData(first?: boolean): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + nearestPoint(point: VecLike): Vec; +} + +// @public (undocumented) +export class EdgeScrollManager { + constructor(editor: Editor); + // (undocumented) + editor: Editor; + // (undocumented) + getIsEdgeScrolling(): boolean; + updateEdgeScrolling(elapsed: number): void; +} + +// @public (undocumented) +export class Editor extends EventEmitter { + constructor({ store, user, shapeUtils, bindingUtils, assetUtils: assetUtilConstructors, overlayUtils: overlayUtilConstructors, tools, getContainer, cameraOptions, initialState, autoFocus, options: _options, textOptions: _textOptions, getShapeVisibility, colorScheme, fontAssetUrls, themes, initialTheme, }: TLEditorOptions); + alignShapes(shapes: TLShape[] | TLShapeId[], operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'center' | 'left' | 'right' | 'top'): this; + animateShape(partial: null | TLShapePartial | undefined, opts?: TLCameraMoveOptions): this; + animateShapes(partials: (null | TLShapePartial | undefined)[], opts?: TLCameraMoveOptions): this; + // @internal (undocumented) + annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: { + extras?: Record; + origin: string; + tags?: Record; + willCrashApp: boolean; + }): this; + assetUtils: { + readonly [K in string]?: AssetUtil; + }; + bail(): this; + bailToMark(id: string): this; + bindingUtils: { + readonly [K in string]?: BindingUtil; + }; + blur({ blurContainer }?: { + blurContainer?: boolean | undefined; + }): this; + bringForward(shapes: TLShape[] | TLShapeId[], opts?: { + considerAllShapes?: boolean; + }): this; + bringToFront(shapes: TLShape[] | TLShapeId[]): this; + // (undocumented) + canBindShapes({ fromShape, toShape, binding, }: { + binding: { + type: TLBinding['type']; + } | TLBinding | TLBinding['type']; + fromShape: { + type: TLShape['type']; + } | TLShape | TLShape['type']; + toShape: { + type: TLShape['type']; + } | TLShape | TLShape['type']; + }): boolean; + cancel(): this; + cancelDoubleClick(): void; + canCreateShape(shape: OptionalKeys, 'id'> | TLShape['id']): boolean; + canCreateShapes(shapes: (OptionalKeys, 'id'> | TLShape['id'])[]): boolean; + canCropShape(shape: null | T): shape is T; + canEditShape(shape: null | T, info?: TLEditStartInfo): shape is T; + canRedo(): boolean; + canUndo(): boolean; + // @internal (undocumented) + capturedPointerId: null | number; + centerOnPoint(point: VecLike, opts?: TLCameraMoveOptions): this; + // (undocumented) + clearHistory(): this; + // @internal + protected _clickManager: ClickManager; + readonly collaborators: CollaboratorsManager; + complete(): this; + // (undocumented) + readonly contextId: string; + // @internal (undocumented) + crash(error: unknown): this; + createAssets(assets: TLAsset[]): this; + createBinding(partial: TLBindingCreate): this; + createBindings(partials: TLBindingCreate[]): this; + createDeepLink(opts?: { + param?: string; + to?: TLDeepLink; + url?: string | URL; + }): URL; + // @internal (undocumented) + createErrorAnnotations(origin: string, willCrashApp: 'unknown' | boolean): { + extras: { + activeStateNode: string; + collaboratorCount: number; + editingShape: TLShape | undefined; + inputs: { + altKey: boolean; + buttons: number[]; + ctrlKey: boolean; + currentPagePoint: VecModel; + currentScreenPoint: VecModel; + isDragging: boolean; + isEditing: boolean; + isPanning: boolean; + isPen: boolean; + isPinching: boolean; + isPointing: boolean; + isSpacebarPanning: boolean; + keys: string[]; + metaKey: boolean; + originPagePoint: VecModel; + originScreenPoint: VecModel; + pointerVelocity: VecModel; + previousPagePoint: VecModel; + previousScreenPoint: VecModel; + shiftKey: boolean; + }; + instanceState: TLInstance; + pageState: TLInstancePageState; + selectedShapes: ({ + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "arrow"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "bookmark"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "draw"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "embed"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "frame"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "geo"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "group"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "highlight"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "image"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "line"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "my-custom-shape"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "note"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "test-persistent"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "test-shape"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "text"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: any; + rotation: number; + type: "video"; + typeName: 'shape'; + x: number; + y: number; + })[]; + selectionCount: number; + }; + tags: { + origin: string; + willCrashApp: "unknown" | boolean; + }; + } | { + extras: { + activeStateNode?: undefined; + collaboratorCount?: undefined; + editingShape?: undefined; + inputs?: undefined; + instanceState?: undefined; + pageState?: undefined; + selectedShapes?: undefined; + selectionCount?: undefined; + }; + tags: { + origin: string; + willCrashApp: "unknown" | boolean; + }; + }; + createPage(page: Partial): this; + createShape(shape: TLCreateShapePartial): this; + createShapes(shapes: TLCreateShapePartial[]): this; + createTemporaryAssetPreview(assetId: TLAssetId, file: File): string | undefined; + deleteAssets(assets: TLAsset[] | TLAssetId[]): this; + deleteBinding(binding: TLBinding | TLBindingId, opts?: Parameters[1]): this; + deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes }?: { + isolateShapes?: boolean | undefined; + }): this; + // @internal + _deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes }?: { + isolateShapes?: boolean | undefined; + }): this; + deletePage(page: TLPage | TLPageId): this; + deleteShape(id: TLShapeId): this; + // (undocumented) + deleteShape(shape: TLShape): this; + deleteShapes(ids: TLShapeId[]): this; + // (undocumented) + deleteShapes(shapes: TLShape[]): this; + deselect(...shapes: TLShape[] | TLShapeId[]): this; + dispatch(info: TLEventInfo): this; + readonly disposables: Set<() => void>; + dispose(): void; + distributeShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this; + duplicatePage(page: TLPage | TLPageId, createId?: TLPageId): this; + duplicateShapes(shapes: TLShape[] | TLShapeId[], offset?: VecLike): this; + edgeScrollManager: EdgeScrollManager; + // @internal + _ensureUserRecord(user: TLUser): void; + // @internal (undocumented) + externalAssetContentHandlers: { + [K in TLExternalAsset['type']]: { + [Key in K]: ((info: TLExternalAsset & { + type: Key; + }) => Promise) | null; + }[K]; + }; + // @internal (undocumented) + externalContentHandlers: { + [K in TLExternalContent['type']]: { + [Key in K]: ((info: Extract, { + type: Key; + }>) => void) | null; + }[K]; + }; + findCommonAncestor(shapes: TLShape[] | TLShapeId[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined; + findShapeAncestor(shape: TLShape | TLShapeId, predicate: (parent: TLShape) => boolean): TLShape | undefined; + flipShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this; + // (undocumented) + _flushEventForTick(info: TLEventInfo): this | undefined; + focus({ focusContainer }?: { + focusContainer?: boolean | undefined; + }): this; + readonly fonts: FontManager; + getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined; + getAsset(asset: T | T['id']): T | undefined; + getAssetForExternalContent(info: TLExternalAsset): Promise; + getAssets(): (TLBookmarkAsset | TLImageAsset | TLVideoAsset)[]; + getAssetUtil(asset: { + type: S['type']; + } | S): AssetUtil; + // (undocumented) + getAssetUtil(type: string): AssetUtil; + getAssetUtilForMimeType(mimeType: string): AssetUtil | null; + getAttributionDisplayName(userId: null | string): null | string; + getAttributionUser(userId: null | string): null | TLUser; + getAttributionUserId(): null | string; + getBaseZoom(): number; + getBinding(id: TLBindingId): TLBinding | undefined; + getBindingsFromShape(shape: TLShape | TLShapeId, type: K): Extract[]; + // (undocumented) + getBindingsFromShape(shape: TLShape | TLShapeId, type: Binding['type']): Binding[]; + getBindingsInvolvingShape(shape: TLShape | TLShapeId, type: K): Extract[]; + // (undocumented) + getBindingsInvolvingShape(shape: TLShape | TLShapeId, type?: Binding['type']): Binding[]; + getBindingsToShape(shape: TLShape | TLShapeId, type: K): Extract[]; + // (undocumented) + getBindingsToShape(shape: TLShape | TLShapeId, type: Binding['type']): Binding[]; + getBindingUtil(type: K): BindingUtil>; + // (undocumented) + getBindingUtil(binding: { + type: S['type']; + } | S): BindingUtil; + // (undocumented) + getBindingUtil(type: T extends BindingUtil ? R['type'] : string): T; + getCamera(): TLCamera; + getCameraOptions(): TLCameraOptions; + getCameraState(): "idle" | "moving"; + // (undocumented) + getCanRedo(): boolean; + // (undocumented) + getCanUndo(): boolean; + getCollaborators(): TLInstancePresence[]; + getCollaboratorsOnCurrentPage(): TLInstancePresence[]; + getColorMode(): 'dark' | 'light'; + getContainer: () => HTMLElement; + // @internal + getContainerDocument(): Document; + // @internal + getContainerWindow(): Window & typeof globalThis; + getContentFromCurrentPage(shapes: TLShape[] | TLShapeId[]): TLContent | undefined; + // @internal + getCrashingError(): unknown; + getCroppingShapeId(): null | TLShapeId; + getCulledShapes(): Set; + getCurrentPage(): TLPage; + getCurrentPageBounds(): Box | undefined; + getCurrentPageId(): TLPageId; + getCurrentPageRenderingShapesSorted(): TLShape[]; + getCurrentPageShapeIds(): Set; + // @internal (undocumented) + getCurrentPageShapeIdsSorted(): TLShapeId[]; + getCurrentPageShapes(): TLShape[]; + getCurrentPageShapesInReadingOrder(): TLShape[]; + getCurrentPageShapesSorted(): TLShape[]; + getCurrentPageState(): TLInstancePageState; + getCurrentTheme(): TLTheme; + getCurrentThemeId(): TLThemeId; + getCurrentTool(): StateNode; + getCurrentToolId(): string; + getDebouncedZoomLevel(): number; + getDocumentSettings(): TLDocument; + getDraggingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined; + getEditingShape(): TLShape | undefined; + getEditingShapeId(): null | TLShapeId; + getEfficientZoomLevel(): number; + getErasingShapeIds(): TLShapeId[]; + getErasingShapes(): NonNullable[]; + getFocusedGroup(): TLShape | undefined; + getFocusedGroupId(): TLPageId | TLShapeId; + getHighestIndexForParent(parent: TLPage | TLParentId | TLShape): IndexKey; + getHintingShape(): NonNullable[]; + getHintingShapeIds(): TLShapeId[]; + getHitTestMargin(): number; + getHoveredShape(): TLShape | undefined; + getHoveredShapeId(): null | TLShapeId; + getInitialMetaForShape(_shape: TLShape): JsonObject; + getInitialZoom(): number; + getInstanceState(): TLInstance; + // (undocumented) + getIsFocused(): boolean; + getIsMounted(): boolean; + // (undocumented) + getIsReadonly(): boolean; + // @internal + getMarkIdMatching(idSubstring: string): null | string; + getNearestAdjacentShape(shapes: TLShape[], currentShapeId: TLShapeId, direction: 'down' | 'left' | 'right' | 'up'): TLShapeId; + getNotVisibleShapes(): Set; + getOnlySelectedShape(): null | TLShape; + getOnlySelectedShapeId(): null | TLShapeId; + getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape; + getPage(page: TLPage | TLPageId): TLPage | undefined; + getPages(): TLPage[]; + getPageShapeIds(page: TLPage | TLPageId): Set; + getPageStates(): TLInstancePageState[]; + getPath(): string; + getPointInParentSpace(shape: TLShape | TLShapeId, point: VecLike): Vec; + getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec; + // @internal + _getReferencedUserIds(shapes: TLShape[]): Set; + getRenderingShapes(): TLRenderingShape[]; + getResizeScaleFactor(): number; + // @internal + getResizeShapePartial(shape: TLShape | TLShapeId, scale: VecLike, opts?: TLResizeShapeOptions): null | TLShapePartial; + getRichTextEditor(): null | TiptapEditor; + getSelectedShapeAtPoint(point: VecLike): TLShape | undefined; + getSelectedShapeIds(): TLShapeId[]; + getSelectedShapes(): TLShape[]; + getSelectionPageBounds(): Box | null; + getSelectionRotatedPageBounds(): Box | undefined; + getSelectionRotatedScreenBounds(): Box | undefined; + getSelectionRotation(): number; + getSelectionScreenBounds(): Box | undefined; + getShape(shape: TLParentId | TLShape): T | undefined; + getShapeAncestors(shape: TLShape | TLShapeId, acc?: TLShape[]): TLShape[]; + getShapeAndDescendantIds(ids: TLShapeId[]): Set; + getShapeAtPoint(point: VecLike, opts?: TLGetShapeAtPointOptions): TLShape | undefined; + getShapeClipPath(shape: TLShape | TLShapeId): string | undefined; + getShapeGeometry(shape: TLShape | TLShapeId, opts?: TLGeometryOpts): T; + getShapeHandles(shape: T | T['id']): TLHandle[] | undefined; + getShapeIdsInsideBounds(bounds: Box): Set; + getShapeLocalTransform(shape: TLShape | TLShapeId): Mat; + getShapeMask(shape: TLShape | TLShapeId): undefined | VecLike[]; + getShapeMaskedPageBounds(shape: TLShape | TLShapeId): Box | undefined; + // @internal + getShapeNearestSibling(siblingShape: TLShape, targetShape: TLShape | undefined): TLShape | undefined; + getShapePageBounds(shape: TLShape | TLShapeId): Box | undefined; + getShapePageTransform(shape: TLShape | TLShapeId): Mat; + getShapeParent(shape?: TLShape | TLShapeId): TLShape | undefined; + getShapeParentTransform(shape: TLShape | TLShapeId): Mat; + getShapesAtPoint(point: VecLike, opts?: { + hitInside?: boolean; + margin?: number; + }): TLShape[]; + getShapesPageBounds(shapeIds: TLShapeId[]): Box | null; + // @internal (undocumented) + getShapesRotatedPageBounds(shapeIds: TLShapeId[]): Box | undefined; + // @internal (undocumented) + getShapesSharedRotation(shapeIds: TLShapeId[]): number; + // (undocumented) + getShapeStyleIfExists(shape: TLShape, style: StyleProp): T | undefined; + getShapeUtil(type: K): ShapeUtil>; + // (undocumented) + getShapeUtil(shape: S | S['type'] | TLShapePartial): ShapeUtil; + // (undocumented) + getShapeUtil(type: T extends ShapeUtil ? R['type'] : string): T; + getShapeUtilForAssetType(assetType: string): ShapeUtil | undefined; + getSharedOpacity(): SharedStyle; + getSharedStyles(): ReadonlySharedStyleMap; + // (undocumented) + getSnapshot(): TLEditorSnapshot; + getSortedChildIdsForParent(parent: TLPage | TLParentId | TLShape): TLShapeId[]; + getStateDescendant(path: string): T | undefined; + getStyleForNextShape(style: StyleProp): T; + getSvgElement(shapes: TLShape[] | TLShapeId[], opts?: TLSvgExportOptions): Promise<{ + height: number; + svg: SVGSVGElement; + trimPadding: number; + width: number; + } | undefined>; + getSvgString(shapes: TLShape[] | TLShapeId[], opts?: TLSvgExportOptions): Promise<{ + height: number; + svg: string; + trimPadding: number; + width: number; + } | undefined>; + getTemporaryAssetPreview(assetId: TLAssetId): string | undefined; + getTextOptions(): TLTextOptions; + getTheme(id: TLThemeId): TLTheme | undefined; + getThemes(): TLThemes; + // @internal (undocumented) + getUnorderedRenderingShapes(useEditorState: boolean): TLRenderingShape[]; + getViewportPageBounds(): Box; + getViewportScreenBounds(): Box; + getViewportScreenCenter(): Vec; + getVisibleCollaborators(): TLInstancePresence[]; + getVisibleCollaboratorsOnCurrentPage(): TLInstancePresence[]; + getZoomLevel(): number; + groupShapes(shapes: TLShape[], opts?: Partial<{ + groupId: TLShapeId; + select: boolean; + }>): this; + // (undocumented) + groupShapes(ids: TLShapeId[], opts?: Partial<{ + groupId: TLShapeId; + select: boolean; + }>): this; + hasAncestor(shape: TLShape | TLShapeId | undefined, ancestorId: TLShapeId): boolean; + hasAssetUtil(arg: { + type: string; + } | string): boolean; + // (undocumented) + hasExternalAssetHandler(type: TLExternalAsset['type']): boolean; + hasShapeUtil(shape: TLShape | TLShapePartial): boolean; + // (undocumented) + hasShapeUtil(type: TLShape['type']): boolean; + // (undocumented) + hasShapeUtil(type: T extends ShapeUtil ? R['type'] : string): boolean; + protected readonly history: HistoryManager; + // (undocumented) + readonly id: string; + readonly inputs: InputsManager; + interrupt(): this; + isAncestorSelected(shape: TLShape | TLShapeId): boolean; + isDisposed: boolean; + isIn(path: string): boolean; + isInAny(...paths: string[]): boolean; + isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: { + hitInside?: boolean; + margin?: number; + }): boolean; + // @internal + isReplayingHistory(): boolean; + isShapeFrameLike(shape: TLShape | TLShapeId): boolean; + // (undocumented) + isShapeHidden(shapeOrId: TLShape | TLShapeId): boolean; + isShapeInPage(shape: TLShape | TLShapeId, pageId?: TLPageId): boolean; + isShapeOfType(shape: TLShape, type: K): shape is Extract; + // (undocumented) + isShapeOfType(shape: TLShape, type: T['type']): shape is Extract; + // (undocumented) + isShapeOfType(shapeId: TLShapeId, type: T['type']): boolean; + isShapeOrAncestorLocked(shape?: TLShape | TLShapeId): boolean; + // @internal + licenseManager?: LicenseManager; + loadSnapshot(snapshot: Partial | TLStoreSnapshot, opts?: TLLoadSnapshotOptions): this; + markEventAsHandled(e: { + nativeEvent: Event; + } | Event): void; + markHistoryStoppingPoint(name?: string): string; + // (undocumented) + menus: { + addOpenMenu: (id: string) => void; + clearOpenMenus: () => void; + deleteOpenMenu: (id: string) => void; + getOpenMenus: () => string[]; + hasAnyOpenMenus: () => boolean; + hasOpenMenus: () => boolean; + isMenuOpen: (id: string) => boolean; + }; + moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this; + navigateToDeepLink(opts?: { + param?: string; + url?: string | URL; + } | TLDeepLink): Editor; + nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike): this; + // (undocumented) + readonly options: TldrawOptions; + readonly overlays: OverlayManager; + packShapes(shapes: TLShape[] | TLShapeId[], _gap?: number): this; + pageToScreen(point: VecLike): Vec; + pageToViewport(point: VecLike): Vec; + readonly performance: PerformanceManager; + popFocusedGroupId(): this; + putContentOntoCurrentPage(content: TLContent, opts?: { + point?: VecLike; + preserveIds?: boolean; + preservePosition?: boolean; + select?: boolean; + }): this; + putExternalContent(info: TLExternalContent, opts?: { + force?: boolean; + }): Promise; + redo(): this; + registerDeepLinkListener(opts?: TLDeepLinkOptions): () => void; + registerExternalAssetHandler(type: T, handler: ((info: TLExternalAsset & { + type: T; + }) => Promise) | null): this; + registerExternalContentHandler['type'], E>(type: T, handler: ((info: T extends TLExternalContent['type'] ? Extract, { + type: T; + }> : TLExternalContent) => void) | null): this; + // @internal + _releaseAltKey(): void; + // @internal + _releaseCtrlKey(): void; + // @internal + _releaseMetaKey(): void; + // @internal + _releaseShiftKey(): void; + removeTool(Tool: TLStateNodeConstructor, parent?: StateNode): void; + renamePage(page: TLPage | TLPageId, name: string): this; + reparentShapes(shapes: TLShape[] | TLShapeId[], parentId: TLParentId, insertIndex?: IndexKey): this; + replaceExternalContent(info: TLExternalContent, opts?: { + force?: boolean; + }): Promise; + resetZoom(point?: Vec, opts?: TLCameraMoveOptions): this; + resizeShape(shape: TLShape | TLShapeId, scale: VecLike, opts?: TLResizeShapeOptions): this; + resizeToBounds(shapes: TLShape[] | TLShapeId[], bounds: BoxLike): this; + // (undocumented) + resolveAssetsInContent(content: TLContent | undefined): Promise; + // (undocumented) + resolveAssetUrl(assetId: null | TLAssetId, context: { + dpr?: number; + screenScale?: number; + shouldResolveToOriginal?: boolean; + }): Promise; + readonly root: StateNode; + rotateShapesBy(shapes: TLShape[] | TLShapeId[], delta: number, opts?: { + center?: VecLike; + }): this; + run(fn: () => void, opts?: TLEditorRunOptions): this; + screenToPage(point: VecLike): Vec; + readonly scribbles: ScribbleManager; + select(...shapes: TLShape[] | TLShapeId[]): this; + selectAdjacentShape(direction: TLAdjacentDirection): void; + selectAll(): this; + // (undocumented) + selectFirstChildShape(): void; + selectNone(): this; + // (undocumented) + selectParentShape(): void; + sendBackward(shapes: TLShape[] | TLShapeId[], opts?: { + considerAllShapes?: boolean; + }): this; + sendToBack(shapes: TLShape[] | TLShapeId[]): this; + setCamera(point: VecLike, opts?: TLCameraMoveOptions): this; + setCameraOptions(opts: Partial): this; + setColorMode(mode: 'dark' | 'light'): this; + setCroppingShape(shape: null | TLShape | TLShapeId): this; + setCurrentPage(page: TLPage | TLPageId): this; + setCurrentTheme(id: TLThemeId): this; + setCurrentTool(id: string, info?: {}): this; + setCursor(cursor: Partial): this; + setEditingShape(shape: null | TLShape | TLShapeId): this; + setErasingShapes(shapes: TLShape[] | TLShapeId[]): this; + setFocusedGroup(shape: null | TLGroupShape | TLShapeId): this; + setHintingShapes(shapes: TLShape[] | TLShapeId[]): this; + setHoveredShape(shape: null | TLShape | TLShapeId): this; + setOpacityForNextShapes(opacity: number, historyOptions?: TLHistoryBatchOptions): this; + setOpacityForSelectedShapes(opacity: number): this; + setRichTextEditor(textEditor: null | TiptapEditor): this; + setSelectedShapes(shapes: TLShape[] | TLShapeId[]): this; + setStyleForNextShapes(style: StyleProp, value: T, historyOptions?: TLHistoryBatchOptions): this; + setStyleForSelectedShapes>(style: S, value: StylePropValue): this; + setTool(Tool: TLStateNodeConstructor, parent?: StateNode): void; + shapeUtils: { + readonly [K in string]?: ShapeUtil; + }; + readonly sideEffects: StoreSideEffects; + slideCamera(opts?: { + direction: VecLike; + force?: boolean; + friction?: number; + speed: number; + speedThreshold?: number; + }): this; + readonly snaps: SnapManager; + squashToMark(markId: string): this; + stackShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical', gap?: number): this; + startFollowingUser(userId: TLUserId): this; + stopCameraAnimation(): this; + stopFollowingUser(): this; + readonly store: TLStore; + stretchShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this; + // (undocumented) + styleProps: { + [key: string]: Map, string>; + }; + readonly textMeasure: TextManager; + readonly timers: { + cancelAnimationFrame: (id: number | undefined) => void; + clearInterval: (id: number | undefined) => void; + clearTimeout: (id: number | undefined) => void; + dispose: () => void; + requestAnimationFrame: (callback: FrameRequestCallback) => number; + setInterval: (handler: TimerHandler, timeout?: number, ...args: any[]) => number; + setTimeout: (handler: TimerHandler, timeout?: number, ...args: any[]) => number; + }; + toggleLock(shapes: TLShape[] | TLShapeId[]): this; + toImage(shapes: TLShape[] | TLShapeId[], opts?: TLImageExportOptions): Promise<{ + blob: Blob; + height: number; + width: number; + }>; + toImageDataUrl(shapes: TLShape[] | TLShapeId[], opts?: TLImageExportOptions): Promise<{ + height: number; + url: string; + width: number; + }>; + undo(): this; + ungroupShapes(ids: TLShapeId[], opts?: Partial<{ + select: boolean; + }>): this; + // (undocumented) + ungroupShapes(shapes: TLShape[], opts?: Partial<{ + select: boolean; + }>): this; + updateAssets(assets: TLAssetPartial[]): this; + updateBinding(partial: TLBindingUpdate): this; + updateBindings(partials: (null | TLBindingUpdate | undefined)[]): this; + updateCurrentPageState(partial: Partial>): this; + // (undocumented) + _updateCurrentPageState(partial: Partial>): void; + updateDocumentSettings(settings: Partial): this; + updateInstanceState(partial: Partial>, historyOptions?: TLHistoryBatchOptions): this; + // @internal (undocumented) + _updateInstanceState(partial: Partial>, opts?: TLHistoryBatchOptions): void; + updatePage(partial: RequiredKeys, 'id'>): this; + updatePointer(options?: TLUpdatePointerOptions): this; + updateShape(partial: null | TLShapePartial | undefined): this; + updateShapes(partials: (null | TLShapePartial | undefined)[]): this; + // @internal (undocumented) + _updateShapes(_partials: (null | TLShapePartial | undefined)[]): void; + updateTheme(theme: TLTheme): this; + updateThemes(themes: ((themes: TLThemes) => TLThemes) | TLThemes): this; + updateViewportScreenBounds(screenBounds: Box | HTMLElement, center?: boolean): this; + uploadAsset(asset: TLAsset, file: File, abortSignal?: AbortSignal): Promise<{ + meta?: JsonObject; + src: string; + }>; + readonly user: UserPreferencesManager; + visitDescendants(parent: TLPage | TLParentId | TLShape, visitor: (id: TLShapeId) => false | void): this; + wasEventAlreadyHandled(e: { + nativeEvent: Event; + } | Event): boolean; + zoomIn(point?: Vec, opts?: TLCameraMoveOptions): this; + zoomOut(point?: Vec, opts?: TLCameraMoveOptions): this; + zoomToBounds(bounds: BoxLike, opts?: { + inset?: number; + targetZoom?: number; + } & TLCameraMoveOptions): this; + zoomToFit(opts?: TLCameraMoveOptions): this; + zoomToSelection(opts?: TLCameraMoveOptions): this; + zoomToSelectionIfOffscreen(padding?: number, opts?: { + inset?: number; + targetZoom?: number; + } & TLCameraMoveOptions): void; + zoomToUser(userId: TLUserId, opts?: TLCameraMoveOptions): this; +} + +// @public +export class EditorAtom { + constructor(name: string, getInitialState: (editor: Editor) => T); + // (undocumented) + get(editor: Editor): T; + // (undocumented) + getAtom(editor: Editor): Atom; + // (undocumented) + set(editor: Editor, state: T): T; + // (undocumented) + update(editor: Editor, update: (state: T) => T): T; +} + +// @public (undocumented) +export const EditorContext: React_3.Context; + +// @public +export abstract class EditorManager { + constructor(editor: Editor); + protected addEditorEvent(event: E, fn: (...args: TLEventMap[E]) => void): () => void; + // (undocumented) + protected readonly disposables: Set<() => void>; + // @internal (undocumented) + dispose(): void; + // (undocumented) + protected readonly editor: Editor; + protected register(dispose: () => void): () => void; + protected unregister(dispose: () => void): void; +} + +// @public +export function EditorPortal({ children }: EditorPortalProps): ReactPortal | null; + +// @public (undocumented) +export interface EditorPortalProps { + // (undocumented) + children: ReactNode; +} + +// @public (undocumented) +export function EditorProvider({ editor, children }: EditorProviderProps): JSX.Element; + +// @public (undocumented) +export interface EditorProviderProps { + // (undocumented) + children: React_3.ReactNode; + // (undocumented) + editor: Editor; +} + +// @internal (undocumented) +export function elementShouldCaptureKeys(el: Element | null, includeButtonsAndMenus?: boolean): boolean; + +// @public (undocumented) +export class Ellipse2d extends Geometry2d { + constructor(config: Omit & { + height: number; + width: number; + }); + // (undocumented) + config: Omit & { + height: number; + width: number; + }; + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean): number; + // (undocumented) + get edges(): Edge2d[]; + // (undocumented) + getBounds(): Box; + // (undocumented) + getLength(): number; + // (undocumented) + getSvgPathData(first?: boolean): string; + // (undocumented) + getVertices(): any[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike): boolean; + // (undocumented) + nearestPoint(A: VecLike): Vec; +} + +// @public (undocumented) +export class ErrorBoundary extends React_2.Component, { + error: Error | null; +}> { + // (undocumented) + componentDidCatch(error: unknown): void; + // (undocumented) + static getDerivedStateFromError(error: Error): { + error: Error; + }; + // (undocumented) + render(): bigint | boolean | JSX.Element | Iterable | null | number | Promise | null | number | React_2.ReactElement | string> | React_2.ReactPortal | string | undefined> | string | undefined; + // (undocumented) + state: { + error: null; + }; +} + +// @public (undocumented) +export function ErrorScreen({ children }: LoadingScreenProps): JSX.Element; + +// @public (undocumented) +export const EVENT_NAME_MAP: Record, keyof TLEventHandlers>; + +// @internal (undocumented) +export function extractSessionStateFromLegacySnapshot(store: Record): null | TLSessionStateSnapshot; + +// @internal (undocumented) +export const featureFlags: Record>; + +// @public (undocumented) +export class FontManager { + constructor(editor: Editor, assetUrls?: { + [key: string]: string | undefined; + } | undefined); + // (undocumented) + dispose(): void; + // (undocumented) + ensureFontIsLoaded(font: TLFontFace): Promise; + // (undocumented) + getShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[]; + // (undocumented) + loadRequiredFontsForCurrentPage(limit?: number): Promise; + // (undocumented) + requestFonts(fonts: TLFontFace[]): void; + // (undocumented) + toEmbeddedCssDeclaration(font: TLFontFace): Promise; + // (undocumented) + trackFontsForShape(shape: TLShape | TLShapeId): void; +} + +// @public (undocumented) +export interface GapsSnapIndicator { + // (undocumented) + direction: 'horizontal' | 'vertical'; + // (undocumented) + gaps: Array<{ + endEdge: [VecLike, VecLike]; + startEdge: [VecLike, VecLike]; + }>; + // (undocumented) + id: string; + // (undocumented) + type: 'gaps'; +} + +// @public (undocumented) +export abstract class Geometry2d { + constructor(opts: Geometry2dOptions); + // (undocumented) + get area(): number; + // (undocumented) + get bounds(): Box; + // (undocumented) + get boundsVertices(): Vec[]; + // (undocumented) + get center(): Vec; + // (undocumented) + debugColor?: string; + // (undocumented) + distanceToLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters): number; + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean, filters?: Geometry2dFilters): number; + // (undocumented) + excludeFromShapeBounds: boolean; + // (undocumented) + getArea(): number; + // (undocumented) + getBounds(): Box; + // (undocumented) + getBoundsVertices(): Vec[]; + // (undocumented) + getLength(_filters?: Geometry2dFilters): number; + // (undocumented) + abstract getSvgPathData(first: boolean): string; + // (undocumented) + abstract getVertices(filters: Geometry2dFilters): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, distance?: number, filters?: Geometry2dFilters): boolean; + // (undocumented) + hitTestPoint(point: VecLike, margin?: number, hitInside?: boolean, _filters?: Geometry2dFilters): boolean; + // (undocumented) + ignore?: boolean; + ignoreHit(_point: VecLike): boolean; + interpolateAlongEdge(t: number, _filters?: Geometry2dFilters): Vec; + // (undocumented) + intersectCircle(center: VecLike, radius: number, _filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectLineSegment(A: VecLike, B: VecLike, _filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectPolygon(polygon: VecLike[], _filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectPolyline(polyline: VecLike[], _filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + isClosed: boolean; + // (undocumented) + isEmptyLabel: boolean; + // (undocumented) + isExcludedByFilter(filters?: Geometry2dFilters): boolean; + // (undocumented) + isFilled: boolean; + // (undocumented) + isInternal: boolean; + // (undocumented) + isLabel: boolean; + // (undocumented) + isPointInBounds(point: VecLike, margin?: number): boolean; + // (undocumented) + get length(): number; + // (undocumented) + abstract nearestPoint(point: VecLike, _filters?: Geometry2dFilters): Vec; + // (undocumented) + overlapsPolygon(_polygon: VecLike[]): boolean; + // (undocumented) + toSimpleSvgPath(): string; + // (undocumented) + transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d; + uninterpolateAlongEdge(point: VecLike, _filters?: Geometry2dFilters): number; + // (undocumented) + get vertices(): Vec[]; +} + +// @public +export interface Geometry2dFilters { + // (undocumented) + readonly includeInternal?: boolean; + // (undocumented) + readonly includeLabels?: boolean; +} + +// @public (undocumented) +export const Geometry2dFilters: { + EXCLUDE_INTERNAL: Geometry2dFilters; + EXCLUDE_LABELS: Geometry2dFilters; + EXCLUDE_NON_STANDARD: Geometry2dFilters; + INCLUDE_ALL: Geometry2dFilters; +}; + +// @public (undocumented) +export interface Geometry2dOptions extends TransformedGeometry2dOptions { + // (undocumented) + isClosed: boolean; + // (undocumented) + isFilled: boolean; +} + +// @public +export function getArcMeasure(A: number, B: number, sweepFlag: number, largeArcFlag: number): number; + +// @public +export function getColorValue(colors: TLThemeColors, color: string | TLDefaultColorStyle, variant: keyof TLDefaultColor): string; + +// @public (undocumented) +export function getCursor(cursor: TLCursorType, rotation?: number, color?: string): string; + +// @public +export function getDefaultCdnBaseUrl(): string; + +// @public +export function getDroppedShapesToNewParents(editor: Editor, shapes: Set | TLShape[], cb?: (shape: TLShape, parent: TLShape) => boolean): { + remainingShapesToReparent: Set; + reparenting: Map; +}; + +// @public (undocumented) +export function getFontsFromRichText(editor: Editor, richText: TLRichText, initialState: RichTextFontVisitorState): TLFontFace[]; + +// @public (undocumented) +export function getFreshUserPreferences(): TLUserPreferences; + +// @internal +export function getGlobalDocument(): Document; + +// @internal +export function getGlobalWindow(): Window & typeof globalThis; + +// @public +export function getIncrementedName(name: string, others: string[]): string; + +// @public +export function getOverlayDisplayValues(util: { + editor: Editor; + options: OverlayOptionsWithDisplayValues; +}, overlay: Overlay, colorMode?: 'dark' | 'light'): DisplayValues; + +// @internal (undocumented) +export function getOwnerDocument(nodeOrDocument: Document | Node | null | undefined): Document; + +// @internal (undocumented) +export function getOwnerWindow(nodeOrDocument: Document | Node | null | undefined): Window & typeof globalThis; + +// @public (undocumented) +export function getPerfectDashProps(totalLength: number, strokeWidth: number, opts?: { + closed?: boolean; + end?: PerfectDashTerminal; + forceSolid?: boolean; + lengthRatio?: number; + snap?: number; + start?: PerfectDashTerminal; + style?: TLDefaultDashStyle; +}): { + strokeDasharray: string; + strokeDashoffset: string; +}; + +// @public (undocumented) +export function getPointerInfo(editor: Editor, e: PointerEvent | React_3.PointerEvent): { + accelKey: boolean; + altKey: boolean; + button: number; + ctrlKey: boolean; + isPen: boolean; + metaKey: boolean; + point: { + x: number; + y: number; + z: number; + }; + pointerId: number; + shiftKey: boolean; +}; + +// @public +export function getPointInArcT(mAB: number, A: number, B: number, P: number): number; + +// @public +export function getPointOnCircle(center: VecLike, r: number, a: number): Vec; + +// @public (undocumented) +export function getPointsOnArc(startPoint: VecLike, endPoint: VecLike, center: null | VecLike, radius: number, numPoints: number): Vec[]; + +// @public (undocumented) +export function getPolygonVertices(width: number, height: number, sides: number): Vec[]; + +// @internal (undocumented) +export function getRotationSnapshot({ editor, ids, }: { + editor: Editor; + ids: TLShapeId[]; +}): null | TLRotationSnapshot; + +// @public (undocumented) +export function getSnapshot(store: TLStore): TLEditorSnapshot; + +// @public (undocumented) +export function getSvgAsImage(svgString: string, options: { + height: number; + pixelRatio?: number; + quality?: number; + type: 'jpeg' | 'png' | 'webp'; + width: number; +}): Promise; + +// @public +export function getSvgPathFromPoints(points: VecLike[], closed?: boolean): string; + +// @public (undocumented) +export function getUserPreferences(): TLUserPreferences; + +// @internal (undocumented) +export function getVerticesCountForArcLength(length: number, spacing?: number): number; + +// @public (undocumented) +export class Group2d extends Geometry2d { + constructor(config: Omit & { + children: Geometry2d[]; + }); + // (undocumented) + children: Geometry2d[]; + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean, filters?: Geometry2dFilters): number; + // (undocumented) + getArea(): number; + // (undocumented) + getBoundsVertices(): Vec[]; + // (undocumented) + getLength(filters?: Geometry2dFilters): number; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(filters: Geometry2dFilters): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, zoom: number, filters?: Geometry2dFilters): boolean; + // (undocumented) + hitTestPoint(point: VecLike, margin: number, hitInside: boolean, filters?: Geometry2dFilters): boolean; + // (undocumented) + ignoredChildren: Geometry2d[]; + // (undocumented) + interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec; + // (undocumented) + intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec; + // (undocumented) + overlapsPolygon(polygon: VecLike[]): boolean; + // (undocumented) + toSimpleSvgPath(): string; + // (undocumented) + transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d; + // (undocumented) + uninterpolateAlongEdge(point: VecLike, filters?: Geometry2dFilters): number; +} + +// @public (undocumented) +export class GroupShapeUtil extends ShapeUtil { + // (undocumented) + canBind(): boolean; + // (undocumented) + canResize(): boolean; + // (undocumented) + canResizeChildren(): boolean; + // (undocumented) + component(shape: TLGroupShape): JSX.Element | null; + // (undocumented) + getDefaultProps(): TLGroupShape['props']; + // (undocumented) + getGeometry(shape: TLGroupShape): Geometry2d; + // (undocumented) + getIndicatorPath(shape: TLGroupShape): Path2D; + // (undocumented) + hideSelectionBoundsFg(shape: TLGroupShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onChildrenChange(group: TLGroupShape): void; + // (undocumented) + static props: RecordProps; + // (undocumented) + static type: 'group'; +} + +// @public (undocumented) +export const HALF_PI: number; + +// @public +export interface HandleSnapGeometry { + getSelfSnapOutline?(handle: TLHandle): Geometry2d | null; + getSelfSnapPoints?(handle: TLHandle): VecModel[]; + outline?: Geometry2d | null; + points?: VecModel[]; +} + +// @public (undocumented) +export class HandleSnaps { + constructor(manager: SnapManager); + // (undocumented) + readonly editor: Editor; + // (undocumented) + readonly manager: SnapManager; + // (undocumented) + snapHandle({ currentShapeId, handle, }: { + currentShapeId: TLShapeId; + handle: TLHandle; + }): null | SnapData; +} + +// @public +export function hardReset({ shouldReload }?: { + shouldReload?: boolean | undefined; +}): Promise; + +// @public (undocumented) +export function hardResetEditor(): void; + +// @public (undocumented) +export class HistoryManager { + constructor(opts: { + annotateError?(error: unknown): void; + store: Store; + }); + // (undocumented) + bail(): this; + // (undocumented) + bailToMark(id: string): this; + // (undocumented) + batch(fn: () => void, opts?: TLHistoryBatchOptions): this; + // (undocumented) + clear(): void; + // @internal (undocumented) + debug(): { + pendingDiff: { + diff: RecordsDiff; + isEmpty: boolean; + }; + redos: TLHistoryEntry[]; + state: string; + undos: TLHistoryEntry[]; + }; + // (undocumented) + readonly dispose: () => void; + // @internal (undocumented) + getMarkIdMatching(idSubstring: string): null | string; + // (undocumented) + getNumRedos(): number; + // (undocumented) + getNumUndos(): number; + // @internal (undocumented) + _isInBatch: boolean; + // @internal (undocumented) + isReplaying(): boolean; + // @internal (undocumented) + _mark(id: string): void; + // (undocumented) + redo(): this; + // (undocumented) + squashToMark(id: string): this; + // (undocumented) + undo(): this; + // (undocumented) + _undo({ pushToRedoStack, toMark }: { + pushToRedoStack: boolean; + toMark?: string; + }): this; +} + +// @public (undocumented) +export function HTMLContainer({ children, className, ...rest }: HTMLContainerProps): JSX.Element; + +// @public (undocumented) +export type HTMLContainerProps = React_2.HTMLAttributes; + +// @public (undocumented) +export const inlineBase64AssetStore: TLAssetStore; + +// @public (undocumented) +export class InputsManager extends EditorManager { + constructor(editor: Editor); + // @deprecated (undocumented) + get accelKey(): boolean; + // @deprecated (undocumented) + get altKey(): boolean; + set altKey(altKey: boolean); + readonly buttons: AtomSet; + // @deprecated (undocumented) + get ctrlKey(): boolean; + set ctrlKey(ctrlKey: boolean); + // @deprecated (undocumented) + get currentPagePoint(): Vec; + // @deprecated (undocumented) + get currentScreenPoint(): Vec; + getAccelKey(): boolean; + getAltKey(): boolean; + getCtrlKey(): boolean; + getCurrentPagePoint(): Vec; + getCurrentScreenPoint(): Vec; + getIsDragging(): boolean; + getIsEditing(): boolean; + getIsPanning(): boolean; + getIsPen(): boolean; + getIsPinching(): boolean; + getIsPointing(): boolean; + getIsRightPointing(): boolean; + getIsSpacebarPanning(): boolean; + getMetaKey(): boolean; + getOriginPagePoint(): Vec; + getOriginScreenPoint(): Vec; + getPointerVelocity(): Vec; + getPreviousPagePoint(): Vec; + getPreviousScreenPoint(): Vec; + getShiftKey(): boolean; + get isDragging(): boolean; + set isDragging(isDragging: boolean); + // @deprecated (undocumented) + get isEditing(): boolean; + set isEditing(isEditing: boolean); + // @deprecated (undocumented) + get isPanning(): boolean; + set isPanning(isPanning: boolean); + // @deprecated (undocumented) + get isPen(): boolean; + set isPen(isPen: boolean); + // @deprecated (undocumented) + get isPinching(): boolean; + set isPinching(isPinching: boolean); + // @deprecated (undocumented) + get isPointing(): boolean; + set isPointing(isPointing: boolean); + // @deprecated (undocumented) + get isSpacebarPanning(): boolean; + set isSpacebarPanning(isSpacebarPanning: boolean); + readonly keys: AtomSet; + markActivity(): void; + // @deprecated (undocumented) + get metaKey(): boolean; + set metaKey(metaKey: boolean); + // @deprecated (undocumented) + get originPagePoint(): Vec; + // @deprecated (undocumented) + get originScreenPoint(): Vec; + // @deprecated (undocumented) + get pointerVelocity(): Vec; + // @deprecated (undocumented) + get previousPagePoint(): Vec; + // @deprecated (undocumented) + get previousScreenPoint(): Vec; + // @internal (undocumented) + setAltKey(altKey: boolean): void; + // @internal (undocumented) + setCtrlKey(ctrlKey: boolean): void; + // (undocumented) + setIsDragging(isDragging: boolean): void; + // (undocumented) + setIsEditing(isEditing: boolean): void; + // @internal (undocumented) + setIsPanning(isPanning: boolean): void; + // (undocumented) + setIsPen(isPen: boolean): void; + // @internal (undocumented) + setIsPinching(isPinching: boolean): void; + // @internal (undocumented) + setIsPointing(isPointing: boolean): void; + // @internal (undocumented) + setIsRightPointing(isRightPointing: boolean): void; + // @internal (undocumented) + setIsSpacebarPanning(isSpacebarPanning: boolean): void; + // @internal (undocumented) + setMetaKey(metaKey: boolean): void; + // @internal + setPointerVelocity(pointerVelocity: Vec): void; + // @internal (undocumented) + setShiftKey(shiftKey: boolean): void; + // @deprecated (undocumented) + get shiftKey(): boolean; + set shiftKey(shiftKey: boolean); + // (undocumented) + toJson(): { + altKey: boolean; + buttons: number[]; + ctrlKey: boolean; + currentPagePoint: VecModel; + currentScreenPoint: VecModel; + isDragging: boolean; + isEditing: boolean; + isPanning: boolean; + isPen: boolean; + isPinching: boolean; + isPointing: boolean; + isSpacebarPanning: boolean; + keys: string[]; + metaKey: boolean; + originPagePoint: VecModel; + originScreenPoint: VecModel; + pointerVelocity: VecModel; + previousPagePoint: VecModel; + previousScreenPoint: VecModel; + shiftKey: boolean; + }; + // @internal + updateFromEvent(info: TLPinchEventInfo | TLPointerEventInfo | TLWheelEventInfo): void; + // @internal + updatePointerVelocity(elapsed: number): void; +} + +// @public +export function intersectCircleCircle(c1: VecLike, r1: number, c2: VecLike, r2: number): Vec[]; + +// @public +export function intersectCirclePolygon(c: VecLike, r: number, points: VecLike[]): null | VecLike[]; + +// @public +export function intersectCirclePolyline(c: VecLike, r: number, points: VecLike[]): null | VecLike[]; + +// @public +export function intersectLineSegmentCircle(a1: VecLike, a2: VecLike, c: VecLike, r: number): null | VecLike[]; + +// @public +export function intersectLineSegmentLineSegment(a1: VecLike, a2: VecLike, b1: VecLike, b2: VecLike, precision?: number): null | Vec; + +// @public +export function intersectLineSegmentPolygon(a1: VecLike, a2: VecLike, points: VecLike[]): null | VecLike[]; + +// @public +export function intersectLineSegmentPolyline(a1: VecLike, a2: VecLike, points: VecLike[]): null | VecLike[]; + +// @public +export function intersectPolygonBounds(points: VecLike[], bounds: Box): null | VecLike[]; + +// @public +export function intersectPolygonPolygon(polygonA: VecLike[], polygonB: VecLike[]): null | VecLike[]; + +// @internal (undocumented) +export interface InvalidLicenseKeyResult { + // (undocumented) + isLicenseParseable: false; + // (undocumented) + reason: InvalidLicenseReason; +} + +// @internal (undocumented) +export type InvalidLicenseReason = 'has-key-development-mode' | 'invalid-license-key' | 'no-key-provided'; + +// @internal +export function isAccelKey(e: { + ctrlKey: boolean; + metaKey: boolean; +}): boolean; + +// @public +export function isCursorInViewport(cursor: VecLike, viewport: { + maxX: number; + maxY: number; + minX: number; + minY: number; +}, zoom: number): boolean; + +// @public +export function isSafeFloat(n: number): boolean; + +// @public +export function kickoutOccludedShapes(editor: Editor, shapeIds: TLShapeId[], opts?: { + filter?(parent: TLShape): boolean; +}): void; + +// @internal (undocumented) +export const LICENSE_TIMEOUT = 5000; + +// @internal +export type LicenseFeatureName = 'collaboration' | 'commenting'; + +// @internal (undocumented) +export type LicenseFromKeyResult = InvalidLicenseKeyResult | ValidLicenseKeyResult; + +// @internal (undocumented) +export interface LicenseInfo { + // (undocumented) + expiryDate: string; + // (undocumented) + flags: number; + // (undocumented) + hosts: string[]; + // (undocumented) + id: string; +} + +// @internal (undocumented) +export class LicenseManager { + constructor(licenseKey: string | undefined, testPublicKey?: string); + // (undocumented) + static className: string; + dispose(): void; + // (undocumented) + featureFlags: Atom, unknown>; + // (undocumented) + getLicenseFromKey(licenseKey?: string): Promise; + // (undocumented) + isCryptoAvailable: boolean; + // (undocumented) + isDevelopment: boolean; + isFeatureEnabled(feature: LicenseFeatureName): boolean; + // (undocumented) + isTest: boolean; + // (undocumented) + state: Atom; + // (undocumented) + verbose: boolean; +} + +// @internal (undocumented) +export type LicenseState = 'expired' | 'licensed-with-watermark' | 'licensed' | 'pending' | 'unlicensed-production' | 'unlicensed'; + +// @public (undocumented) +export function linesIntersect(A: VecLike, B: VecLike, C: VecLike, D: VecLike): boolean; + +// @public (undocumented) +export function LoadingScreen({ children }: LoadingScreenProps): JSX.Element; + +// @public (undocumented) +export interface LoadingScreenProps { + // (undocumented) + children: ReactNode; +} + +// @public +export function loadSessionStateSnapshotIntoStore(store: TLStore, snapshot: TLSessionStateSnapshot, opts?: TLLoadSessionStateSnapshotOptions): void; + +// @public +export function loadSnapshot(store: TLStore, _snapshot: Partial | TLStoreSnapshot, opts?: TLLoadSnapshotOptions): void; + +// @internal (undocumented) +export class LocalIndexedDb { + constructor(persistenceKey: string); + // (undocumented) + close(): Promise; + // (undocumented) + static connectedInstances: Set; + // (undocumented) + getAsset(assetId: string): Promise; + // (undocumented) + load({ sessionId }?: { + sessionId?: string; + }): Promise<{ + records: any[]; + schema: any; + sessionStateSnapshot: TLSessionStateSnapshot | undefined; + }>; + pending(): Promise; + pruneSessions({ keepSessionId }?: { + keepSessionId?: string; + }): Promise; + // (undocumented) + removeAssets(assetId: string[]): Promise; + // (undocumented) + storeAsset(assetId: string, blob: File): Promise; + // (undocumented) + storeChanges({ schema, changes, sessionId, sessionStateSnapshot, }: { + changes: RecordsDiff; + schema: TLStoreSchema; + sessionId?: null | string; + sessionStateSnapshot?: null | TLSessionStateSnapshot; + }): Promise; + // (undocumented) + storeSnapshot({ schema, snapshot, sessionId, sessionStateSnapshot, }: { + schema: TLStoreSchema; + sessionId?: null | string; + sessionStateSnapshot?: null | TLSessionStateSnapshot; + snapshot: SerializedStore; + }): Promise; +} + +// @public (undocumented) +export function loopToHtmlElement(elm: Element): HTMLElement; + +// @public (undocumented) +export class Mat { + constructor(a: number, b: number, c: number, d: number, e: number, f: number); + // (undocumented) + a: number; + // (undocumented) + static Absolute(m: MatLike): MatModel; + // (undocumented) + static applyToBounds(m: MatLike, box: Box): Box; + // (undocumented) + applyToPoint(point: VecLike): Vec; + // (undocumented) + static applyToPoint(m: MatLike, point: VecLike): Vec; + // (undocumented) + applyToPoints(points: VecLike[]): Vec[]; + // (undocumented) + static applyToPoints(m: MatLike, points: VecLike[]): Vec[]; + // (undocumented) + static applyToXY(m: MatLike, x: number, y: number): number[]; + // (undocumented) + b: number; + // (undocumented) + c: number; + // (undocumented) + static Cast(m: MatLike): Mat; + // (undocumented) + clone(): Mat; + // (undocumented) + static Compose(...matrices: MatLike[]): Mat; + // (undocumented) + d: number; + // (undocumented) + static Decompose(m: MatLike): { + rotation: number; + scaleX: number; + scaleY: number; + x: number; + y: number; + }; + // (undocumented) + decompose(): { + rotation: number; + scaleX: number; + scaleY: number; + x: number; + y: number; + }; + // (undocumented) + decomposed(): { + rotation: number; + scaleX: number; + scaleY: number; + x: number; + y: number; + }; + // (undocumented) + e: number; + // (undocumented) + equals(m: Mat | MatModel): boolean; + // (undocumented) + f: number; + // (undocumented) + static From(m: MatLike): Mat; + // (undocumented) + static Identity(): Mat; + // (undocumented) + identity(): this; + // (undocumented) + static Inverse(m: MatModel): MatModel; + // (undocumented) + invert(): this; + // (undocumented) + static Multiply(m1: MatModel, m2: MatModel): MatModel; + // (undocumented) + multiply(m: Mat | MatModel): this; + // (undocumented) + static Point(m: MatLike): Vec; + // (undocumented) + point(): Vec; + // (undocumented) + static Rotate(r: number, cx?: number, cy?: number): Mat; + // (undocumented) + rotate(r: number, cx?: number, cy?: number): Mat; + // (undocumented) + static Rotation(m: MatLike): number; + // (undocumented) + rotation(): number; + // (undocumented) + static Scale(x: number, y: number): Mat; + // (undocumented) + static Scale(x: number, y: number, cx: number, cy: number): Mat; + // (undocumented) + scale(x: number, y: number): this; + // (undocumented) + setTo(model: MatModel): this; + // (undocumented) + static Smooth(m: MatLike, precision?: number): MatLike; + // (undocumented) + toCssString(): string; + // (undocumented) + static toCssString(m: MatLike): string; + // (undocumented) + static Translate(x: number, y: number): Mat; + // (undocumented) + translate(x: number, y: number): Mat; +} + +// @public (undocumented) +export type MatLike = Mat | MatModel; + +// @public (undocumented) +export interface MatModel { + // (undocumented) + a: number; + // (undocumented) + b: number; + // (undocumented) + c: number; + // (undocumented) + d: number; + // (undocumented) + e: number; + // (undocumented) + f: number; +} + +// @public +export function maybeSnapToGrid(point: Vec, editor: Editor): Vec; + +// @public +export function MenuClickCapture(): false | JSX.Element; + +// @public +export function moveElementInto(parent: HTMLElement, element: HTMLElement): void; + +// @internal +export function normalizeWheel(event: React.WheelEvent | WheelEvent): { + x: number; + y: number; + z: number; +}; + +// @public +export function openWindow(url: string, target?: string, allowReferrer?: boolean): void; + +// @internal (undocumented) +export function OptionalErrorBoundary({ children, fallback, ...props }: Omit & { + fallback: TLErrorFallbackComponent; +}): bigint | boolean | JSX.Element | Iterable | null | number | Promise | null | number | React_2.ReactElement | string> | React_2.ReactPortal | string | undefined> | string | undefined; + +// @public (undocumented) +export type OptionalKeys = Omit & Partial>; + +// @public (undocumented) +export class OverlayManager { + constructor(editor: Editor); + // (undocumented) + dispose(): void; + // (undocumented) + readonly editor: Editor; + getActiveOverlayEntries(): TLOverlayEntry[]; + getCurrentOverlays(): TLOverlay[]; + // (undocumented) + getHoveredOverlay(): null | TLOverlay; + // (undocumented) + getHoveredOverlayId(): null | string; + getOverlayAtPoint(point: VecLike, margin?: number): null | TLOverlay; + getOverlayGeometry(overlay: TLOverlay): Geometry2d | null; + getOverlayUtil(type: T extends OverlayUtil ? O['type'] : string): T; + // (undocumented) + getOverlayUtil(overlay: O): OverlayUtil; + getOverlayUtilsInZOrder(): OverlayUtil[]; + // @internal (undocumented) + readonly _overlayUtils: Map>>>; + // @internal + registerUtil(util: OverlayUtil): void; + // (undocumented) + setHoveredOverlay(id: null | string): void; +} + +// @public (undocumented) +export interface OverlayOptionsWithDisplayValues { + // (undocumented) + getCustomDisplayValues(editor: Editor, overlay: Overlay, theme: TLTheme, colorMode: 'dark' | 'light'): Partial; + // (undocumented) + getDefaultDisplayValues(editor: Editor, overlay: Overlay, theme: TLTheme, colorMode: 'dark' | 'light'): DisplayValues; +} + +// @public +export abstract class OverlayUtil { + constructor(editor: Editor); + static configure>(this: T, options: T extends new (...args: any[]) => { + options: infer Options; + } ? Partial : never): T; + dispose(): void; + // (undocumented) + editor: Editor; + getCursor(_overlay: T): TLCursorType | undefined; + getGeometry(_overlay: T): Geometry2d | null; + abstract getOverlays(): T[]; + abstract isActive(): boolean; + onPointerDown?(overlay: T, info: TLPointerEventInfo): boolean | void; + options: { + zIndex?: number; + }; + render(_ctx: CanvasRenderingContext2D, _overlays: T[]): void; + renderMinimap(_ctx: CanvasRenderingContext2D, _overlays: T[], _zoom: number): void; + // (undocumented) + static type: string; +} + +// @public +export function parseDeepLinkString(deepLinkString: string): TLDeepLink; + +// @public (undocumented) +export type PerfectDashTerminal = 'none' | 'outset' | 'skip'; + +// @public +export class PerformanceApiAdapter { + constructor(perfManager: PerformanceManager); + dispose(): void; +} + +// @public +export class PerformanceManager extends EditorManager { + // @internal (undocumented) + dispose(): void; + // @internal (undocumented) + readonly emitter: EventEmitter; + // @internal (undocumented) + _notifyCameraOperation(type: 'panning' | 'zooming'): void; + // @internal (undocumented) + _notifyInteractionEnd(): void; + // @internal (undocumented) + _notifyInteractionStart(name: string, path: string): void; + // @internal (undocumented) + _notifyUndoRedo(type: 'redo' | 'undo', undoDepth: number, redoDepth: number): void; + on(event: K, fn: (...args: TLPerfEventMap[K]) => void): () => void; + once(event: K, fn: (...args: TLPerfEventMap[K]) => void): () => void; +} + +// @public +export function perimeterOfEllipse(rx: number, ry: number): number; + +// @public (undocumented) +export const PI: number; + +// @public (undocumented) +export const PI2: number; + +// @public (undocumented) +export class Point2d extends Geometry2d { + constructor(config: Omit & { + margin: number; + point: Vec; + }); + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, margin: number): boolean; + // (undocumented) + nearestPoint(): Vec; +} + +// @public +export function pointInPolygon(A: VecLike, points: VecLike[]): boolean; + +// @public (undocumented) +export interface PointsSnapIndicator { + // (undocumented) + id: string; + // (undocumented) + points: VecLike[]; + // (undocumented) + type: 'points'; +} + +// @public (undocumented) +export class Polygon2d extends Polyline2d { + constructor(config: Omit & { + points: Vec[]; + }); +} + +// @public (undocumented) +export function polygonIntersectsPolyline(polygon: VecLike[], polyline: VecLike[]): boolean; + +// @public (undocumented) +export function polygonsIntersect(a: VecLike[], b: VecLike[]): boolean; + +// @public (undocumented) +export class Polyline2d extends Geometry2d { + constructor(config: Omit & { + points: Vec[]; + }); + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean): number; + // (undocumented) + getLength(): number; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, distance?: number): boolean; + // (undocumented) + hitTestPoint(point: VecLike, margin?: number, hitInside?: boolean): boolean; + // (undocumented) + nearestPoint(A: VecLike): Vec; + // (undocumented) + protected get segments(): Edge2d[]; +} + +// @public (undocumented) +export function precise(A: VecLike): string; + +// @public +export function preventDefault(event: Event | React.BaseSyntheticEvent): void; + +// @public +export function radiansToDegrees(r: number): number; + +// @public +export function rangeIntersection(a0: number, a1: number, b0: number, b1: number): [number, number] | null; + +// @public +export class ReadonlySharedStyleMap { + // (undocumented) + [Symbol.iterator](): MapIterator<[StyleProp, SharedStyle]>; + constructor(entries?: Iterable<[StyleProp, SharedStyle]>); + // (undocumented) + entries(): MapIterator<[StyleProp, SharedStyle]>; + // (undocumented) + equals(other: ReadonlySharedStyleMap): boolean; + // (undocumented) + get(prop: StyleProp): SharedStyle | undefined; + // (undocumented) + getAsKnownValue(prop: StyleProp): T | undefined; + // (undocumented) + keys(): MapIterator>; + // @internal (undocumented) + protected map: Map, SharedStyle>; + // (undocumented) + get size(): number; + // (undocumented) + values(): MapIterator>; +} + +// @public (undocumented) +export class Rectangle2d extends Polygon2d { + constructor(config: Omit & { + height: number; + width: number; + x?: number; + y?: number; + }); + // (undocumented) + getBounds(): Box; + // (undocumented) + getSvgPathData(): string; +} + +// @public (undocumented) +export function refreshPage(): void; + +// @public (undocumented) +export function releasePointerCapture(element: Element, event: PointerEvent | React.PointerEvent): void; + +// @public (undocumented) +export type RequiredKeys = Required> & Omit; + +// @public (undocumented) +export function resizeBox(shape: T, info: { + handle: TLResizeHandle; + initialBounds: Box; + initialShape: T; + mode: TLResizeMode; + newPoint: VecModel; + scaleX: number; + scaleY: number; +}, opts?: ResizeBoxOptions): T; + +// @public (undocumented) +export interface ResizeBoxOptions { + // (undocumented) + maxHeight?: number; + // (undocumented) + maxWidth?: number; + // (undocumented) + minHeight?: number; + // (undocumented) + minWidth?: number; +} + +// @public +export function resizeScaled(shape: TLBaseShape, { initialBounds, scaleX, scaleY, newPoint, handle }: TLResizeInfo): { + props: { + scale: number; + }; + x: number; + y: number; +}; + +// @public +export function resolveLineHeightPx(fontSize: number, lineHeight: number): number; + +// @public +export function resolveThemes(themes?: Partial): TLThemes; + +// @public (undocumented) +export type RichTextFontVisitor = (node: TiptapNode, state: RichTextFontVisitorState, addFont: (font: TLFontFace) => void) => RichTextFontVisitorState; + +// @public (undocumented) +export interface RichTextFontVisitorState { + // (undocumented) + readonly family: string; + // (undocumented) + readonly style: string; + // (undocumented) + readonly weight: string; +} + +// @public (undocumented) +export const ROTATE_CORNER_TO_SELECTION_CORNER: { + readonly bottom_left_rotate: 'bottom_left'; + readonly bottom_right_rotate: 'bottom_right'; + readonly mobile_rotate: 'top_left'; + readonly top_left_rotate: 'top_left'; + readonly top_right_rotate: 'top_right'; +}; + +// @public (undocumented) +export type RotateCorner = 'bottom_left_rotate' | 'bottom_right_rotate' | 'mobile_rotate' | 'top_left_rotate' | 'top_right_rotate'; + +// @public (undocumented) +export function rotateSelectionHandle(handle: SelectionHandle, rotation: number): SelectionHandle; + +// @public (undocumented) +export const runtime: { + hardReset(): Promise; + openWindow(url: string, target: string, allowReferrer?: boolean): void; + refreshPage(): void; +}; + +// @public +export type SafeId = string & { + __brand: 'SafeId'; +}; + +// @public (undocumented) +export function sanitizeId(id: string): string; + +// @public (undocumented) +export interface ScribbleItem { + // (undocumented) + delayRemaining: number; + // (undocumented) + id: string; + // (undocumented) + next: null | VecModel; + // (undocumented) + prev: null | VecModel; + // (undocumented) + scribble: TLScribble; + // (undocumented) + timeoutMs: number; +} + +// @public (undocumented) +export class ScribbleManager { + constructor(editor: Editor); + addPoint(id: string, x: number, y: number, z?: number): ScribbleItem; + addPointToSession(sessionId: string, scribbleId: string, x: number, y: number, z?: number): ScribbleItem; + addScribble(scribble: Partial, id?: string): ScribbleItem; + addScribbleToSession(sessionId: string, scribble: Partial, scribbleId?: string): ScribbleItem; + clearSession(sessionId: string): void; + complete(id: string): ScribbleItem; + extendSession(sessionId: string): void; + isSessionActive(sessionId: string): boolean; + reset(): void; + startSession(options?: ScribbleSessionOptions): string; + stop(id: string): ScribbleItem; + stopSession(sessionId: string): void; + tick(elapsed: number): void; +} + +// @public (undocumented) +export interface ScribbleSessionOptions { + fadeDurationMs?: number; + fadeEasing?: 'ease-in' | 'linear'; + fadeMode?: 'grouped' | 'individual'; + id?: string; + idleTimeoutMs?: number; + selfConsume?: boolean; +} + +// @public (undocumented) +export type SelectionCorner = 'bottom_left' | 'bottom_right' | 'top_left' | 'top_right'; + +// @public (undocumented) +export type SelectionEdge = 'bottom' | 'left' | 'right' | 'top'; + +// @public (undocumented) +export type SelectionHandle = SelectionCorner | SelectionEdge; + +// @public (undocumented) +export function setPointerCapture(element: Element, event: PointerEvent | React.PointerEvent): void; + +// @public (undocumented) +export function setRuntimeOverrides(input: Partial): void; + +// @public (undocumented) +export function setUserPreferences(user: TLUserPreferences): void; + +// @public (undocumented) +export abstract class ShapeUtil { + constructor(editor: Editor); + // @internal + backgroundComponent?(shape: Shape): any; + canBeLaidOut(shape: Shape, info: TLShapeUtilCanBeLaidOutOpts): boolean; + canBind(opts: TLShapeUtilCanBindOpts): boolean; + canCrop(shape: Shape): boolean; + canCull(shape: Shape): boolean; + canEdit(shape: Shape, info: TLEditStartInfo): boolean; + canEditInReadonly(shape: Shape): boolean; + canEditWhileLocked(shape: Shape): boolean; + canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']): boolean; + canRemoveChildrenOfType(shape: Shape, type: TLShape['type']): boolean; + canResize(shape: Shape): boolean; + canResizeChildren(shape: Shape): boolean; + canScroll(shape: Shape): boolean; + canSnap(shape: Shape): boolean; + canTabTo(shape: Shape): boolean; + abstract component(shape: Shape): any; + static configure>(this: T, options: T extends new (...args: any[]) => { + options: infer Options; + } ? Partial : never): T; + createShapeForAsset?(asset: TLAsset, position: VecModel): null | TLShapePartial; + // (undocumented) + editor: Editor; + // @internal (undocumented) + expandSelectionOutlinePx(shape: Shape): Box | number; + getAppOwnedElement?(shape: Shape): HTMLElement | null; + // (undocumented) + getAriaDescriptor(shape: Shape): string | undefined; + getBoundsSnapGeometry(shape: Shape): BoundsSnapGeometry; + getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[]; + getClipPath?(shape: Shape): undefined | Vec[]; + abstract getDefaultProps(): Shape['props']; + getFontFaces(shape: Shape): TLFontFace[]; + abstract getGeometry(shape: Shape, opts?: TLGeometryOpts): Geometry2d; + getHandles?(shape: Shape): TLHandle[]; + getHandleSnapGeometry(shape: Shape): HandleSnapGeometry; + abstract getIndicatorPath(shape: Shape): TLIndicatorPath | undefined; + getInterpolatedProps?(startShape: Shape, endShape: Shape, progress: number): Shape['props']; + getReferencedUserIds(shape: Shape): string[]; + // (undocumented) + getText(shape: Shape): string | undefined; + static handledAssetTypes?: readonly string[]; + hideInMinimap?(shape: Shape): boolean; + hideResizeHandles(shape: Shape): boolean; + hideRotateHandle(shape: Shape): boolean; + hideSelectionBoundsBg(shape: Shape): boolean; + hideSelectionBoundsFg(shape: Shape): boolean; + // @deprecated + indicator(_shape: Shape): any; + isAspectRatioLocked(shape: Shape): boolean; + isExportBoundsContainer(shape: Shape): boolean; + isFrameLike(_shape: Shape): boolean; + static migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations; + onBeforeCreate?(next: Shape): Shape | void; + onBeforeDuplicate?(source: Shape, duplicate: Shape): Shape | void; + onBeforeUpdate?(prev: Shape, next: Shape): Shape | void; + // @internal + onBindingChange?(shape: Shape): TLShapePartial | void; + onChildrenChange?(shape: Shape): TLShapePartial[] | void; + onClick?(shape: Shape): TLShapePartial | void; + onCrop?(shape: Shape, info: TLCropInfo): Omit, 'id' | 'type'> | undefined | void; + onDoubleClick?(shape: Shape): TLShapePartial | void; + onDoubleClickCorner?(shape: Shape, info: TLClickEventInfo): TLShapePartial | void; + onDoubleClickEdge?(shape: Shape, info: TLClickEventInfo): TLShapePartial | void; + onDoubleClickHandle?(shape: Shape, handle: TLHandle): TLShapePartial | void; + onDragShapesIn?(shape: Shape, shapes: TLShape[], info: TLDragShapesInInfo): void; + onDragShapesOut?(shape: Shape, shapes: TLShape[], info: TLDragShapesOutInfo): void; + onDragShapesOver?(shape: Shape, shapes: TLShape[], info: TLDragShapesOverInfo): void; + onDropShapesOver?(shape: Shape, shapes: TLShape[], info: TLDropShapesOverInfo): void; + onEditEnd?(shape: Shape): void; + onEditStart?(shape: Shape): void; + onHandleDrag?(shape: Shape, info: TLHandleDragInfo): TLShapePartial | void; + onHandleDragCancel?(current: Shape, info: TLHandleDragInfo): void; + onHandleDragEnd?(current: Shape, info: TLHandleDragInfo): TLShapePartial | void; + onHandleDragStart?(shape: Shape, info: TLHandleDragInfo): TLShapePartial | void; + onReleaseAppOwnedElement?(shape: Shape, element: HTMLElement): void; + onResize?(shape: Shape, info: TLResizeInfo): Omit, 'id' | 'type'> | undefined | void; + onResizeCancel?(initial: Shape, current: Shape): void; + onResizeEnd?(initial: Shape, current: Shape): TLShapePartial | void; + onResizeStart?(shape: Shape): TLShapePartial | void; + onRotate?(initial: Shape, current: Shape): TLShapePartial | void; + onRotateCancel?(initial: Shape, current: Shape): void; + onRotateEnd?(initial: Shape, current: Shape): TLShapePartial | void; + onRotateStart?(shape: Shape): TLShapePartial | void; + onTranslate?(initial: Shape, current: Shape): TLShapePartial | void; + onTranslateCancel?(initial: Shape, current: Shape): void; + onTranslateEnd?(initial: Shape, current: Shape): TLShapePartial | void; + onTranslateStart?(shape: Shape): TLShapePartial | void; + options: {}; + static props?: RecordProps; + // @internal + providesBackgroundForChildren(shape: Shape): boolean; + shouldClipChild?(child: TLShape): boolean; + toBackgroundSvg?(shape: Shape, ctx: SvgExportContext): null | Promise | ReactElement; + toSvg?(shape: Shape, ctx: SvgExportContext): null | Promise | ReactElement; + static type: string; +} + +// @public +export type SharedStyle = { + readonly type: 'mixed'; +} | { + readonly type: 'shared'; + readonly value: T; +}; + +// @internal (undocumented) +export class SharedStyleMap extends ReadonlySharedStyleMap { + // (undocumented) + applyValue(prop: StyleProp, value: T): void; + // (undocumented) + set(prop: StyleProp, value: SharedStyle): void; +} + +// @public +export function shortAngleDist(a0: number, a1: number): number; + +// @public (undocumented) +export const SIDES: readonly ['top', 'right', 'bottom', 'left']; + +// @public (undocumented) +export const SIN: (x: number) => number; + +// @public +export function snapAngle(r: number, segments: number): number; + +// @public (undocumented) +export interface SnapData { + // (undocumented) + nudge: Vec; +} + +// @public (undocumented) +export type SnapIndicator = GapsSnapIndicator | PointsSnapIndicator; + +// @public (undocumented) +export class SnapManager { + constructor(editor: Editor); + // (undocumented) + clearIndicators(): void; + // (undocumented) + readonly editor: Editor; + // (undocumented) + getCurrentCommonAncestor(): TLShapeId | undefined; + // (undocumented) + getIndicators(): SnapIndicator[]; + // (undocumented) + getSnappableShapes(): Set; + // (undocumented) + getSnapThreshold(): number; + // (undocumented) + readonly handles: HandleSnaps; + // (undocumented) + setIndicators(indicators: SnapIndicator[]): void; + // (undocumented) + readonly shapeBounds: BoundsSnaps; +} + +// @internal +export class SpatialIndexManager { + constructor(editor: Editor); + // @public + dispose(): void; + // (undocumented) + readonly editor: Editor; + // @public + getShapeIdsAtPoint(point: { + x: number; + y: number; + }, margin?: number): Set; + // @public + getShapeIdsInsideBounds(bounds: Box): Set; +} + +// @public (undocumented) +export class Stadium2d extends Geometry2d { + constructor(config: Omit & { + height: number; + width: number; + }); + // (undocumented) + config: Omit & { + height: number; + width: number; + }; + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean): number; + // (undocumented) + getBounds(): Box; + // (undocumented) + getLength(): number; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike): boolean; + // (undocumented) + nearestPoint(A: VecLike): Vec; +} + +// @public (undocumented) +export abstract class StateNode implements Partial { + constructor(editor: Editor, parent?: StateNode); + addChild(childConstructor: TLStateNodeConstructor): this; + // (undocumented) + static children?: () => TLStateNodeConstructor[]; + // (undocumented) + children?: Record; + _currentToolIdMask: Atom; + // (undocumented) + editor: Editor; + // (undocumented) + enter(info: any, from: string): void; + // (undocumented) + exit(info: any, to: string): void; + getCurrent(): StateNode | undefined; + // (undocumented) + getCurrentToolIdMask(): string | undefined; + getIsActive(): boolean; + getPath(): string; + // (undocumented) + handleEvent(info: Exclude): void; + // (undocumented) + static id: string; + // (undocumented) + id: string; + // (undocumented) + static initial?: string; + // (undocumented) + initial?: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + isLockable: boolean; + // (undocumented) + onCancel?(info: TLCancelEventInfo): void; + // (undocumented) + onComplete?(info: TLCompleteEventInfo): void; + // (undocumented) + onDoubleClick?(info: TLClickEventInfo): void; + // (undocumented) + onEnter?(info: any, from: string): void; + // (undocumented) + onExit?(info: any, to: string): void; + // (undocumented) + onInterrupt?(info: TLInterruptEventInfo): void; + // (undocumented) + onKeyDown?(info: TLKeyboardEventInfo): void; + // (undocumented) + onKeyRepeat?(info: TLKeyboardEventInfo): void; + // (undocumented) + onKeyUp?(info: TLKeyboardEventInfo): void; + // (undocumented) + onLongPress?(info: TLPointerEventInfo): void; + // (undocumented) + onMiddleClick?(info: TLPointerEventInfo): void; + // (undocumented) + onPointerDown?(info: TLPointerEventInfo): void; + // (undocumented) + onPointerMove?(info: TLPointerEventInfo): void; + // (undocumented) + onPointerUp?(info: TLPointerEventInfo): void; + // (undocumented) + onRightClick?(info: TLPointerEventInfo): void; + // (undocumented) + onTick?(info: TLTickEventInfo): void; + // (undocumented) + onWheel?(info: TLWheelEventInfo): void; + // (undocumented) + parent: StateNode; + // (undocumented) + _path: Computed; + // (undocumented) + performanceTracker: PerformanceTracker; + // (undocumented) + setCurrentToolIdMask(id: string | undefined): void; + // (undocumented) + shapeType?: string; + static trackPerformance: boolean; + transition(id: string, info?: any): this; + // (undocumented) + type: 'branch' | 'leaf' | 'root'; + // (undocumented) + static useCoalescedEvents: boolean; + // (undocumented) + useCoalescedEvents: boolean; +} + +// @public @deprecated +export function stopEventPropagation(e: any): any; + +// @internal (undocumented) +export type StoreName = (typeof Table)[keyof typeof Table]; + +// @public +export function strokeShapeIndicators(editor: Editor, ctx: CanvasRenderingContext2D, shapeIds: TLShapeId[]): void; + +// @public (undocumented) +export function suffixSafeId(id: SafeId, suffix: string): SafeId; + +// @public (undocumented) +export function SVGContainer({ children, className, ...rest }: SVGContainerProps): JSX.Element; + +// @public (undocumented) +export type SVGContainerProps = React_2.ComponentProps<'svg'>; + +// @public (undocumented) +export interface SvgExportContext { + addExportDef(def: SvgExportDef): void; + readonly colorMode: 'dark' | 'light'; + readonly isDarkMode: boolean; + readonly pixelRatio: null | number; + resolveAssetUrl(assetId: TLAssetId, width: number): Promise; + readonly scale: number; + waitUntil(promise: Promise): void; +} + +// @public (undocumented) +export interface SvgExportDef { + // (undocumented) + getElement(): null | Promise | ReactElement; + // (undocumented) + key: string; +} + +// @public +export const TAB_ID: string; + +// @internal (undocumented) +export const Table: { + readonly Assets: 'assets'; + readonly Records: 'records'; + readonly Schema: 'schema'; + readonly SessionState: 'session_state'; +}; + +// @public (undocumented) +export class TextManager extends EditorManager { + constructor(editor: Editor); + measureElementTextNodeSpans(element: HTMLElement, { shouldTruncateToFirstLine }?: { + shouldTruncateToFirstLine?: boolean; + }): { + didTruncate: boolean; + spans: { + box: BoxModel; + text: string; + }[]; + }; + // (undocumented) + measureHtml(html: string, opts: TLMeasureTextOpts): TLMeasuredTextSize; + // (undocumented) + measureHtmlBatch(requests: BatchMeasurementRequest[]): TLMeasuredTextSize[]; + // (undocumented) + measureText(textToMeasure: string, opts: TLMeasureTextOpts): TLMeasuredTextSize; + measureTextSpans(textToMeasure: string, opts: TLMeasureTextSpanOpts): { + box: BoxModel; + text: string; + }[]; +} + +// @public +export class ThemeManager { + constructor(editor: Editor, options: { + initial: TLThemeId; + themes: TLThemes; + }); + dispose(): void; + getColorMode(): 'dark' | 'light'; + // (undocumented) + getCurrentTheme(): TLTheme; + getCurrentThemeId(): TLThemeId; + getTheme(id: TLThemeId): TLTheme | undefined; + getThemes(): TLThemes; + setCurrentTheme(id: TLThemeId): void; + updateTheme(theme: TLTheme): void; + updateThemes(themes: ((themes: TLThemes) => TLThemes) | TLThemes): void; +} + +// @internal (undocumented) +export class TickManager extends EditorManager { + constructor(editor: Editor); + // (undocumented) + cancelRaf?: (() => void) | null; + // (undocumented) + dispose(): void; + // (undocumented) + isPaused: boolean; + // (undocumented) + now: number; + // (undocumented) + start(): void; + // (undocumented) + tick(): void; +} + +// @public +export type TiptapEditor = Editor_2; + +// @public +export type TiptapNode = Node_2; + +// @public (undocumented) +export type TLAdjacentDirection = 'down' | 'left' | 'next' | 'prev' | 'right' | 'up'; + +// @public (undocumented) +export type TLAnyAssetUtilConstructor = TLAssetUtilConstructor; + +// @public (undocumented) +export type TLAnyBindingUtilConstructor = TLBindingUtilConstructor; + +// @public (undocumented) +export type TLAnyOverlayUtilConstructor = TLOverlayUtilConstructor; + +// @public (undocumented) +export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor; + +// @public (undocumented) +export interface TLAssetUtilConstructor = AssetUtil> { + // (undocumented) + new (editor: Editor): U; + // (undocumented) + migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations; + // (undocumented) + props?: RecordProps; + // (undocumented) + type: T['type']; +} + +// @public (undocumented) +export type TLBaseBoxShape = ExtractShapeByProps<{ + h: number; + w: number; +}>; + +// @public (undocumented) +export interface TLBaseEventInfo { + // (undocumented) + accelKey: boolean; + // (undocumented) + altKey: boolean; + // (undocumented) + ctrlKey: boolean; + // (undocumented) + metaKey: boolean; + // (undocumented) + shiftKey: boolean; + // (undocumented) + type: UiEventType; +} + +// @public (undocumented) +export interface TLBaseExternalContent { + // (undocumented) + point?: VecLike; + sources?: TLExternalContentSource[]; +} + +// @public (undocumented) +export interface TLBindingUtilConstructor = BindingUtil> { + // (undocumented) + new (editor: Editor): U; + migrations?: TLPropsMigrations; + props?: RecordProps; + // (undocumented) + type: T['type']; +} + +// @public (undocumented) +export interface TLCameraConstraints { + baseZoom: 'default' | 'fit-max-100' | 'fit-max' | 'fit-min-100' | 'fit-min' | 'fit-x-100' | 'fit-x' | 'fit-y-100' | 'fit-y'; + behavior: 'contain' | 'fixed' | 'free' | 'inside' | 'outside' | { + x: 'contain' | 'fixed' | 'free' | 'inside' | 'outside'; + y: 'contain' | 'fixed' | 'free' | 'inside' | 'outside'; + }; + bounds: BoxModel; + initialZoom: 'default' | 'fit-max-100' | 'fit-max' | 'fit-min-100' | 'fit-min' | 'fit-x-100' | 'fit-x' | 'fit-y-100' | 'fit-y'; + origin: VecLike; + padding: VecLike; +} + +// @public +export interface TLCameraEndPerfEvent extends TLPerfFrameTimeStats { + culledShapeCount: number; + shapeCount: number; + timestamp: number; + type: 'panning' | 'zooming'; + viewportHeight: number; + viewportWidth: number; + visibleShapeCount: number; + zoomLevel: number; +} + +// @public (undocumented) +export interface TLCameraMoveOptions { + animation?: { + easing?(t: number): number; + duration?: number; + }; + force?: boolean; + immediate?: boolean; + reset?: boolean; +} + +// @public (undocumented) +export interface TLCameraOptions { + constraints?: TLCameraConstraints; + isLocked: boolean; + panSpeed: number; + wheelBehavior: 'none' | 'pan' | 'zoom'; + zoomSpeed: number; + zoomSteps: number[]; +} + +// @public +export interface TLCameraStartPerfEvent { + timestamp: number; + type: 'panning' | 'zooming'; +} + +// @public (undocumented) +export type TLCancelEvent = (info: TLCancelEventInfo) => void; + +// @public (undocumented) +export interface TLCancelEventInfo { + // (undocumented) + name: 'cancel'; + // (undocumented) + type: 'misc'; +} + +// @public (undocumented) +export interface TLCanvasComponentProps { + // (undocumented) + className?: string; +} + +// @public (undocumented) +export type TLClickEvent = (info: TLClickEventInfo) => void; + +// @public (undocumented) +export type TLClickEventInfo = TLBaseEventInfo & { + button: number; + name: TLCLickEventName; + phase: 'down' | 'settle-down' | 'settle-up' | 'up'; + point: VecLike; + pointerId: number; + type: 'click'; +} & TLPointerEventTarget; + +// @public (undocumented) +export type TLCLickEventName = 'double_click'; + +// @public (undocumented) +export type TLClickState = 'idle' | 'overflow' | 'pendingDouble' | 'pendingOverflow'; + +// @public +export type TLClipboardPasteRawInfo = { + readonly clipboardData: DataTransfer | null; + readonly editor: Editor; + readonly event: ClipboardEvent; + readonly point: undefined | VecLike; + readonly source: 'native-event'; +} | { + readonly clipboardItems: readonly ClipboardItem[]; + readonly editor: Editor; + readonly point: undefined | VecLike; + readonly source: 'clipboard-read'; +}; + +// @public +export interface TLClipboardWriteInfo { + // (undocumented) + readonly operation: 'copy' | 'cut'; + // (undocumented) + readonly source: 'menu' | 'native'; +} + +// @public (undocumented) +export type TLCompleteEvent = (info: TLCompleteEventInfo) => void; + +// @public (undocumented) +export interface TLCompleteEventInfo { + // (undocumented) + name: 'complete'; + // (undocumented) + type: 'misc'; +} + +// @public (undocumented) +export interface TLContent { + // (undocumented) + assets: TLAsset[]; + // (undocumented) + bindings: TLBinding[] | undefined; + // (undocumented) + rootShapeIds: TLShapeId[]; + // (undocumented) + schema: SerializedSchema; + // (undocumented) + shapes: TLShape[]; + // (undocumented) + users?: TLUser[]; +} + +// @public +export interface TLCropInfo { + // (undocumented) + aspectRatioLocked?: boolean; + // (undocumented) + change: Vec; + // (undocumented) + crop: TLShapeCrop; + // (undocumented) + handle: SelectionHandle; + // (undocumented) + initialShape: T; + // (undocumented) + isResizingFromCenter?: boolean; + // (undocumented) + uncroppedSize: { + h: number; + w: number; + }; +} + +// @public (undocumented) +export interface TLCurrentUser { + // (undocumented) + readonly setUserPreferences: (userPreferences: TLUserPreferences) => void; + // (undocumented) + readonly userPreferences: Signal; +} + +// @public (undocumented) +export interface TLCursorProps { + // (undocumented) + chatMessage: string; + // (undocumented) + className?: string; + // (undocumented) + color?: string; + // (undocumented) + name: null | string; + // (undocumented) + point: null | VecModel; + // (undocumented) + userId: string; + // (undocumented) + zoom: number; +} + +// @public (undocumented) +export type TLDeepLink = { + bounds: BoxModel; + pageId?: TLPageId; + type: 'viewport'; +} | { + pageId: TLPageId; + type: 'page'; +} | { + shapeIds: TLShapeId[]; + type: 'shapes'; +}; + +// @public (undocumented) +export interface TLDeepLinkOptions { + debounceMs?: number; + getTarget?(editor: Editor): TLDeepLink; + getUrl?(editor: Editor): string | URL; + onChange?(url: URL, editor: Editor): void; + param?: string; +} + +// @public (undocumented) +export interface TLDragShapesInInfo { + // (undocumented) + initialDraggingOverShapeId: null | TLShapeId; + // (undocumented) + initialIndices: Map; + // (undocumented) + initialParentIds: Map; + // (undocumented) + prevDraggingOverShapeId: null | TLShapeId; +} + +// @public (undocumented) +export interface TLDragShapesOutInfo { + // (undocumented) + initialDraggingOverShapeId: null | TLShapeId; + // (undocumented) + initialIndices: Map; + // (undocumented) + initialParentIds: Map; + // (undocumented) + nextDraggingOverShapeId: null | TLShapeId; +} + +// @public (undocumented) +export interface TLDragShapesOverInfo { + // (undocumented) + initialDraggingOverShapeId: null | TLShapeId; + // (undocumented) + initialIndices: Map; + // (undocumented) + initialParentIds: Map; +} + +// @public +export const TldrawEditor: React_3.MemoExoticComponent<({ store, components, className, user: _user, options: _options, textOptions: _textOptions, deepLinks: _deepLinks, ...rest }: TldrawEditorProps) => JSX.Element>; + +// @public +export interface TldrawEditorBaseProps { + assetUrls?: { + fonts?: { + [key: string]: string | undefined; + }; + }; + assetUtils?: readonly TLAnyAssetUtilConstructor[]; + autoFocus?: boolean; + bindingUtils?: readonly TLAnyBindingUtilConstructor[]; + // @deprecated + cameraOptions?: Partial; + children?: ReactNode; + className?: string; + colorScheme?: 'dark' | 'light' | 'system'; + components?: TLEditorComponents; + // @deprecated + deepLinks?: TLDeepLinkOptions | true; + getShapeVisibility?(shape: TLShape, editor: Editor): 'hidden' | 'inherit' | 'visible' | null | undefined; + initialState?: string; + initialTheme?: TLThemeId; + licenseKey?: string; + onMount?: TLOnMountHandler; + options?: Partial; + overlayUtils?: readonly TLAnyOverlayUtilConstructor[]; + shapeUtils?: readonly TLAnyShapeUtilConstructor[]; + // @deprecated + textOptions?: TLTextOptions; + themes?: Partial; + tools?: readonly TLStateNodeConstructor[]; + user?: TLCurrentUser; +} + +// @public (undocumented) +export type TldrawEditorProps = TldrawEditorBaseProps & TldrawEditorStoreProps; + +// @public (undocumented) +export type TldrawEditorStoreProps = TldrawEditorWithoutStoreProps | TldrawEditorWithStoreProps; + +// @public +export interface TldrawEditorWithoutStoreProps extends TLStoreBaseOptions { + migrations?: readonly MigrationSequence[]; + persistenceKey?: string; + // (undocumented) + sessionId?: string; + snapshot?: TLEditorSnapshot | TLStoreSnapshot; + // (undocumented) + store?: undefined; +} + +// @public +export interface TldrawEditorWithStoreProps { + store: TLStore | TLStoreWithStatus; +} + +// @public +export interface TldrawOptions { + // (undocumented) + readonly actionShortcutsLocation: 'menu' | 'swap' | 'toolbar'; + // (undocumented) + readonly adjacentShapeMargin: number; + // (undocumented) + readonly animationMediumMs: number; + readonly branding?: string; + readonly camera: Partial; + // (undocumented) + readonly cameraMovingTimeoutMs: number; + // (undocumented) + readonly cameraSlideFriction: number; + // (undocumented) + readonly coarseDragDistanceSquared: number; + // (undocumented) + readonly coarseHandleRadius: number; + // (undocumented) + readonly coarseHitTestMargin: number; + // (undocumented) + readonly coarsePointerWidth: number; + // (undocumented) + readonly collaboratorCheckIntervalMs: number; + // (undocumented) + readonly collaboratorIdleTimeoutMs: number; + // (undocumented) + readonly collaboratorInactiveTimeoutMs: number; + // (undocumented) + readonly createTextOnCanvasDoubleClick: boolean; + readonly debouncedZoom: boolean; + readonly debouncedZoomThreshold: number; + readonly deepLinks: TLDeepLinkOptions | true | undefined; + // (undocumented) + readonly defaultSvgPadding: number; + // (undocumented) + readonly doubleClickDurationMs: number; + // (undocumented) + readonly dragDistanceSquared: number; + // (undocumented) + readonly edgeScrollDelay: number; + // (undocumented) + readonly edgeScrollDistance: number; + // (undocumented) + readonly edgeScrollEaseDuration: number; + // (undocumented) + readonly edgeScrollSpeed: number; + readonly enableToolbarKeyboardShortcuts: boolean; + experimental__onDropOnCanvas?(options: { + event: React.DragEvent; + point: VecLike; + }): boolean; + readonly exportProvider: ComponentType<{ + children: React.ReactNode; + }>; + // (undocumented) + readonly flattenImageBoundsExpand: number; + // (undocumented) + readonly flattenImageBoundsPadding: number; + // (undocumented) + readonly followChaseViewportSnap: number; + // (undocumented) + readonly gridSteps: readonly { + readonly mid: number; + readonly min: number; + readonly step: number; + }[]; + // (undocumented) + readonly handleRadius: number; + // (undocumented) + readonly hitTestMargin: number; + // (undocumented) + readonly laserDelayMs: number; + readonly laserFadeoutMs: number; + // (undocumented) + readonly longPressDurationMs: number; + // (undocumented) + readonly maxExportDelayMs: number; + // (undocumented) + readonly maxFilesAtOnce: number; + readonly maxFontsToLoadBeforeRender: number; + // (undocumented) + readonly maxPages: number; + // (undocumented) + readonly maxShapesPerPage: number; + // (undocumented) + readonly multiClickDurationMs: number; + readonly nonce: string | undefined; + onBeforeCopyToClipboard?(info: { + content: TLContent; + editor: Editor; + } & TLClipboardWriteInfo): Awaitable; + onBeforePasteFromClipboard?(info: { + content: TLExternalContent; + editor: Editor; + point?: VecLike; + source: 'clipboard-read' | 'native-event'; + }): Awaitable | void>; + onClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void; + readonly quickZoomPreservesScreenBounds: boolean; + readonly rightClickPanning: boolean; + readonly selectLockedShapes: boolean; + readonly snapThreshold: number; + readonly spacebarPanning: boolean; + readonly temporaryAssetPreviewLifetimeMs: number; + readonly text: TLTextOptions; + // (undocumented) + readonly textShadowLod: number; + // (undocumented) + readonly tooltipDelayMs: number; + // (undocumented) + readonly uiCoarseDragDistanceSquared: number; + // (undocumented) + readonly uiDragDistanceSquared: number; + readonly zoomToFitPadding: number; +} + +// @public (undocumented) +export interface TLDropShapesOverInfo { + // (undocumented) + initialDraggingOverShapeId: null | TLShapeId; + // (undocumented) + initialIndices: Map; + // (undocumented) + initialParentIds: Map; +} + +// @public (undocumented) +export interface TLEditorComponents { + // (undocumented) + Background?: ComponentType | null; + // (undocumented) + Canvas?: ComponentType | null; + // (undocumented) + CollaboratorCursor?: ComponentType | null; + // (undocumented) + ErrorFallback?: TLErrorFallbackComponent; + // (undocumented) + Grid?: ComponentType | null; + // (undocumented) + InFrontOfTheCanvas?: ComponentType | null; + // (undocumented) + LoadingScreen?: ComponentType | null; + // (undocumented) + OnTheCanvas?: ComponentType | null; + // (undocumented) + ShapeErrorFallback?: TLShapeErrorFallbackComponent; + // (undocumented) + ShapeWrapper?: ComponentType> | null; + // (undocumented) + Spinner?: ComponentType | null; + // (undocumented) + SvgDefs?: ComponentType | null; +} + +// @public (undocumented) +export interface TLEditorOptions { + assetUtils?: readonly TLAnyAssetUtilConstructor[]; + autoFocus?: boolean; + bindingUtils: readonly TLAnyBindingUtilConstructor[]; + // @deprecated + cameraOptions?: Partial; + colorScheme?: 'dark' | 'light' | 'system'; + // (undocumented) + fontAssetUrls?: { + [key: string]: string | undefined; + }; + getContainer(): HTMLElement; + getShapeVisibility?(shape: TLShape, editor: Editor): 'hidden' | 'inherit' | 'visible' | null | undefined; + initialState?: string; + initialTheme?: TLThemeId; + // (undocumented) + licenseKey?: string; + options?: Partial; + overlayUtils?: readonly TLAnyOverlayUtilConstructor[]; + shapeUtils: readonly TLAnyShapeUtilConstructor[]; + store: TLStore; + // @deprecated + textOptions?: TLTextOptions; + themes?: Partial; + tools: readonly TLStateNodeConstructor[]; + user?: TLCurrentUser; +} + +// @public +export interface TLEditorRunOptions extends TLHistoryBatchOptions { + // (undocumented) + ignoreShapeLock?: boolean; +} + +// @public +export const tleditors: { + mounted: Signal; + getMounted(): readonly Editor[]; +}; + +// @public (undocumented) +export interface TLEditorSnapshot { + // (undocumented) + document: TLStoreSnapshot; + // (undocumented) + session: TLSessionStateSnapshot; +} + +// @public (undocumented) +export interface TLEditStartInfo { + // (undocumented) + type: 'click-header' | 'click' | 'double-click-corner' | 'double-click-edge' | 'double-click' | 'press_enter' | 'unknown'; +} + +// @public (undocumented) +export interface TLEmbedExternalContent extends TLBaseExternalContent { + // (undocumented) + embed: EmbedDefinition; + // (undocumented) + type: 'embed'; + // (undocumented) + url: string; +} + +// @public (undocumented) +export type TLEnterEventHandler = (info: any, from: string) => void; + +// @public +export const tlenv: { + hasCanvasSupport: boolean; + isAndroid: boolean; + isChromeForIos: boolean; + isDarwin: boolean; + isFirefox: boolean; + isIos: boolean; + isSafari: boolean; + isTouchDevice: boolean; + isWebview: boolean; +}; + +// @public +export const tlenvReactive: Atom< { +isCoarsePointer: boolean; +supportsP3ColorSpace: boolean; +}, unknown>; + +// @public (undocumented) +export interface TLErrorBoundaryProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + fallback: TLErrorFallbackComponent; + // (undocumented) + onError?: ((error: unknown) => void) | null; +} + +// @public (undocumented) +export interface TLErrorExternalContentSource { + // (undocumented) + data: null | string; + // (undocumented) + reason: string; + // (undocumented) + type: 'error'; +} + +// @public (undocumented) +export type TLErrorFallbackComponent = ComponentType; + +// @public (undocumented) +export interface TLErrorFallbackProps { + editor?: Editor; + error: unknown; +} + +// @public (undocumented) +export interface TLEventHandlers { + // (undocumented) + onCancel: TLCancelEvent; + // (undocumented) + onComplete: TLCompleteEvent; + // (undocumented) + onDoubleClick: TLClickEvent; + // (undocumented) + onInterrupt: TLInterruptEvent; + // (undocumented) + onKeyDown: TLKeyboardEvent; + // (undocumented) + onKeyRepeat: TLKeyboardEvent; + // (undocumented) + onKeyUp: TLKeyboardEvent; + // (undocumented) + onLongPress: TLPointerEvent; + // (undocumented) + onMiddleClick: TLPointerEvent; + // (undocumented) + onPointerDown: TLPointerEvent; + // (undocumented) + onPointerMove: TLPointerEvent; + // (undocumented) + onPointerUp: TLPointerEvent; + // (undocumented) + onRightClick: TLPointerEvent; + // (undocumented) + onTick: TLTickEvent; + // (undocumented) + onWheel: TLWheelEvent; +} + +// @public (undocumented) +export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEventInfo | TLInterruptEventInfo | TLKeyboardEventInfo | TLPinchEventInfo | TLPointerEventInfo | TLTickEventInfo | TLWheelEventInfo; + +// @public (undocumented) +export interface TLEventMap { + // (undocumented) + 'before-event': [TLEventInfo]; + // (undocumented) + 'created-shapes': [TLRecord[]]; + // (undocumented) + 'deleted-shapes': [TLShapeId[]]; + // (undocumented) + 'edited-shapes': [TLRecord[]]; + // (undocumented) + 'max-shapes': [{ + count: number; + name: string; + pageId: TLPageId; + }]; + // (undocumented) + 'place-caret': [{ + point: { + x: number; + y: number; + }; + shapeId: TLShapeId; + }]; + // (undocumented) + 'select-all-text': [{ + shapeId: TLShapeId; + }]; + // (undocumented) + 'stop-camera-animation': []; + // (undocumented) + 'stop-following': []; + // (undocumented) + change: [HistoryEntry]; + // (undocumented) + crash: [{ + error: unknown; + }]; + // (undocumented) + dispose: []; + // (undocumented) + edit: []; + // (undocumented) + event: [TLEventInfo]; + // (undocumented) + frame: [number]; + // (undocumented) + mount: []; + // (undocumented) + resize: [BoxModel]; + // (undocumented) + tick: [number]; + // (undocumented) + unmount: []; + // (undocumented) + update: []; +} + +// @public (undocumented) +export type TLEventMapHandler = (...args: TLEventMap[T]) => void; + +// @public (undocumented) +export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'tick' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName; + +// @public (undocumented) +export interface TLExcalidrawExternalContent extends TLBaseExternalContent { + // (undocumented) + content: any; + // (undocumented) + type: 'excalidraw'; +} + +// @public (undocumented) +export interface TLExcalidrawExternalContentSource { + // (undocumented) + data: any; + // (undocumented) + type: 'excalidraw'; +} + +// @public (undocumented) +export type TLExitEventHandler = (info: any, to: string) => void; + +// @public (undocumented) +export type TLExportType = 'jpeg' | 'png' | 'svg' | 'webp'; + +// @public (undocumented) +export type TLExternalAsset = TLFileExternalAsset | TLUrlExternalAsset; + +// @public (undocumented) +export type TLExternalContent = TLEmbedExternalContent | TLExcalidrawExternalContent | TLFileReplaceExternalContent | TLFilesExternalContent | TLSvgTextExternalContent | TLTextExternalContent | TLTldrawExternalContent | TLUrlExternalContent; + +// @public (undocumented) +export type TLExternalContentSource = TLErrorExternalContentSource | TLExcalidrawExternalContentSource | TLTextExternalContentSource | TLTldrawExternalContentSource; + +// @public (undocumented) +export interface TLFileExternalAsset { + // (undocumented) + assetId?: TLAssetId; + // (undocumented) + file: File; + // (undocumented) + type: 'file'; +} + +// @public (undocumented) +export interface TLFileReplaceExternalContent extends TLBaseExternalContent { + // (undocumented) + file: File; + // @deprecated (undocumented) + isImage: boolean; + // (undocumented) + shapeId: TLShapeId; + // (undocumented) + type: 'file-replace'; +} + +// @public (undocumented) +export interface TLFilesExternalContent extends TLBaseExternalContent { + // (undocumented) + files: File[]; + // (undocumented) + ignoreParent?: boolean; + // (undocumented) + type: 'files'; +} + +// @public +export interface TLFramePerfEvent { + culledShapeCount: number; + elapsed: number; + shapeCount: number; + visibleShapeCount: number; +} + +// @public +export interface TLGeometryOpts { + context?: string; +} + +// @public +export interface TLGetShapeAtPointOptions { + filter?(shape: TLShape): boolean; + hitFrameInside?: boolean; + hitInside?: boolean; + hitLabels?: boolean; + hitLocked?: boolean; + margin?: [number, number] | number; + renderingOnly?: boolean; +} + +// @public (undocumented) +export interface TLGridProps { + // (undocumented) + size: number; + // (undocumented) + x: number; + // (undocumented) + y: number; + // (undocumented) + z: number; +} + +// @public (undocumented) +export interface TLHandleDragInfo { + // (undocumented) + handle: TLHandle; + // (undocumented) + initial?: T | undefined; + // (undocumented) + isCreatingShape: boolean; + // (undocumented) + isPrecise: boolean; +} + +// @public (undocumented) +export interface TLHistoryBatchOptions { + history?: 'ignore' | 'record-preserveRedoStack' | 'record'; +} + +// @public (undocumented) +export interface TLHistoryDiff { + // (undocumented) + diff: RecordsDiff; + // (undocumented) + type: 'diff'; +} + +// @public (undocumented) +export type TLHistoryEntry = TLHistoryDiff | TLHistoryMark; + +// @public (undocumented) +export interface TLHistoryMark { + // (undocumented) + id: string; + // (undocumented) + type: 'stop'; +} + +// @public (undocumented) +export interface TLImageExportOptions extends TLSvgExportOptions { + format?: TLExportType; + quality?: number; +} + +// @public +export type TLIndicatorPath = { + additionalPaths?: Path2D[]; + clipPath?: Path2D; + path: Path2D; +} | Path2D; + +// @public +export interface TLInteractionEndPerfEvent extends TLPerfFrameTimeStats { + name: string; + path: string; + selectedShapeTypes: Record; + shapeCount: number; + timestamp: number; + zoomLevel: number; +} + +// @public +export interface TLInteractionStartPerfEvent { + name: string; + path: string; + timestamp: number; +} + +// @public (undocumented) +export type TLInterruptEvent = (info: TLInterruptEventInfo) => void; + +// @public (undocumented) +export interface TLInterruptEventInfo { + // (undocumented) + name: 'interrupt'; + // (undocumented) + type: 'misc'; +} + +// @public (undocumented) +export type TLKeyboardEvent = (info: TLKeyboardEventInfo) => void; + +// @public (undocumented) +export type TLKeyboardEventInfo = TLBaseEventInfo & { + code: string; + key: string; + name: TLKeyboardEventName; + type: 'keyboard'; +}; + +// @public (undocumented) +export type TLKeyboardEventName = 'key_down' | 'key_repeat' | 'key_up'; + +// @public +export interface TLLoadSessionStateSnapshotOptions { + forceOverwrite?: boolean; +} + +// @public +export interface TLLoadSnapshotOptions { + forceOverwriteSessionState?: boolean; +} + +// @public (undocumented) +export type TLMeasuredTextSize = BoxModel & { + scrollWidth: number; +}; + +// @public (undocumented) +export interface TLMeasureTextOpts { + // (undocumented) + disableOverflowWrapBreaking?: boolean; + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + fontStyle: string; + // (undocumented) + fontWeight: string; + lineHeight: number; + maxWidth: null | number; + // (undocumented) + measureScrollWidth?: boolean; + // (undocumented) + minWidth?: null | number; + // (undocumented) + otherStyles?: Record; + // (undocumented) + padding: string; +} + +// @public (undocumented) +export interface TLMeasureTextSpanOpts { + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + fontStyle: string; + // (undocumented) + fontWeight: string; + // (undocumented) + height: number; + // (undocumented) + lineHeight: number; + // (undocumented) + measureScrollWidth?: boolean; + // (undocumented) + otherStyles?: Record; + // (undocumented) + overflow: 'truncate-clip' | 'truncate-ellipsis' | 'wrap'; + // (undocumented) + padding: number; + // (undocumented) + textAlign: TLDefaultHorizontalAlignStyle; + // (undocumented) + width: number; +} + +// @public (undocumented) +export const tlmenus: { + _hiddenMenus: string[]; + menus: Atom; + addOpenMenu(id: string, contextId?: string): void; + clearOpenMenus(contextId?: string): void; + deleteOpenMenu(id: string, contextId?: string): void; + getOpenMenus(contextId?: string): string[]; + isMenuOpen(id: string, contextId?: string): boolean; + hasOpenMenus(contextId: string): boolean; + hasAnyOpenMenus(): boolean; + hideOpenMenus(contextId?: string): void; + showOpenMenus(contextId?: string): void; + forContext(contextId: string): { + addOpenMenu: (id: string) => void; + clearOpenMenus: () => void; + deleteOpenMenu: (id: string) => void; + getOpenMenus: () => string[]; + hasAnyOpenMenus: () => boolean; + hasOpenMenus: () => boolean; + isMenuOpen: (id: string) => boolean; + }; +}; + +// @public +export type TLOnMountHandler = (editor: Editor) => (() => undefined | void) | undefined | void; + +// @public (undocumented) +export interface TLOverlay> { + id: string; + props: Props; + type: string; +} + +// @public +export interface TLOverlayEntry { + // (undocumented) + overlays: TLOverlay[]; + // (undocumented) + util: OverlayUtil; +} + +// @public (undocumented) +export interface TLOverlayUtilConstructor { + // (undocumented) + new (editor: Editor): U; + // (undocumented) + configure>(this: T, options: T extends new (...args: any[]) => { + options: infer Options; + } ? Partial : never): T; + // (undocumented) + type: string; +} + +// @public +export interface TLPerfEventMap { + 'camera-end': [TLCameraEndPerfEvent]; + 'camera-start': [TLCameraStartPerfEvent]; + 'interaction-end': [TLInteractionEndPerfEvent]; + 'interaction-start': [TLInteractionStartPerfEvent]; + 'shapes-created': [TLShapeOperationPerfEvent]; + 'shapes-deleted': [TLShapeOperationPerfEvent]; + 'shapes-updated': [TLShapeOperationPerfEvent]; + frame: [TLFramePerfEvent]; + redo: [TLUndoRedoPerfEvent]; + undo: [TLUndoRedoPerfEvent]; +} + +// @public +export interface TLPerfFrameTimeStats { + avgFrameTime: number; + duration: number; + fps: number; + frameCount: number; + frameTimes: number[]; + longAnimationFrames?: TLPerfLongAnimationFrame[]; + maxFrameTime: number; + medianFrameTime: number; + minFrameTime: number; + p95FrameTime: number; + p99FrameTime: number; +} + +// @public +export interface TLPerfLongAnimationFrame { + blockingDuration: number; + duration: number; + scripts: TLPerfLongAnimationFrameScript[]; + startTime: number; +} + +// @public +export interface TLPerfLongAnimationFrameScript { + duration: number; + invoker: string; + sourceURL: string; +} + +// @public (undocumented) +export type TLPinchEvent = (info: TLPinchEventInfo) => void; + +// @public (undocumented) +export type TLPinchEventInfo = TLBaseEventInfo & { + delta: VecModel; + name: TLPinchEventName; + point: VecModel; + type: 'pinch'; +}; + +// @public (undocumented) +export type TLPinchEventName = 'pinch_end' | 'pinch_start' | 'pinch'; + +// @public (undocumented) +export type TLPointerEvent = (info: TLPointerEventInfo) => void; + +// @public (undocumented) +export type TLPointerEventInfo = TLBaseEventInfo & { + isPenDirect?: boolean; + button: number; + isPen: boolean; + name: TLPointerEventName; + point: VecLike; + pointerId: number; + type: 'pointer'; +} & TLPointerEventTarget; + +// @public (undocumented) +export type TLPointerEventName = 'long_press' | 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click'; + +// @public (undocumented) +export type TLPointerEventTarget = { + handle: TLHandle; + shape: TLShape; + target: 'handle'; +} | { + handle?: TLSelectionHandle; + shape?: undefined; + target: 'selection'; +} | { + overlay: TLOverlay; + shape?: undefined; + target: 'overlay'; +} | { + shape: TLShape; + target: 'shape'; +} | { + shape?: undefined; + target: 'canvas'; +}; + +// @public (undocumented) +export interface TLRenderingShape { + // (undocumented) + backgroundIndex: number; + // (undocumented) + id: TLShapeId; + // (undocumented) + index: number; + // (undocumented) + opacity: number; + // (undocumented) + shape: TLShape; + // (undocumented) + util: ShapeUtil; +} + +// @public (undocumented) +export type TLResizeHandle = SelectionCorner | SelectionEdge; + +// @public +export interface TLResizeInfo { + // (undocumented) + handle: TLResizeHandle; + // (undocumented) + initialBounds: Box; + // (undocumented) + initialShape: T; + // (undocumented) + mode: TLResizeMode; + // (undocumented) + newPoint: Vec; + // (undocumented) + scaleX: number; + // (undocumented) + scaleY: number; +} + +// @public +export type TLResizeMode = 'resize_bounds' | 'scale_shape'; + +// @public (undocumented) +export type TLResizeShapeOptions = Partial<{ + dragHandle: TLResizeHandle; + initialBounds: Box; + initialPageTransform: MatLike; + initialShape: TLShape; + isAspectRatioLocked: boolean; + mode: TLResizeMode; + scaleAxisRotation: number; + scaleOrigin: VecLike; + skipStartAndEndCallbacks: boolean; +}>; + +// @internal (undocumented) +export interface TLRotationSnapshot { + // (undocumented) + initialCursorAngle: number; + // (undocumented) + initialPageCenter: Vec; + // (undocumented) + initialShapesRotation: number; + // (undocumented) + shapeSnapshots: { + initialPagePoint: Vec; + shape: TLShape; + }[]; +} + +// @public (undocumented) +export type TLSelectionHandle = RotateCorner | SelectionCorner | SelectionEdge; + +// @public +export interface TLSessionStateSnapshot { + // (undocumented) + currentPageId?: TLPageId; + // (undocumented) + exportBackground?: boolean; + // (undocumented) + isDebugMode?: boolean; + // (undocumented) + isFocusMode?: boolean; + // (undocumented) + isGridMode?: boolean; + // (undocumented) + isToolLocked?: boolean; + // (undocumented) + pageStates?: Array<{ + camera?: { + x: number; + y: number; + z: number; + }; + focusedGroupId?: null | TLShapeId; + pageId: TLPageId; + selectedShapeIds?: TLShapeId[]; + }>; + // (undocumented) + version: number; +} + +// @public (undocumented) +export type TLShapeErrorFallbackComponent = ComponentType<{ + error: any; +}>; + +// @public +export interface TLShapeOperationPerfEvent { + count: number; + operation: 'create' | 'delete' | 'update'; + shapeTypes: Record; + timestamp: number; +} + +// @public +export interface TLShapeUtilCanBeLaidOutOpts { + shapes?: TLShape[]; + type?: 'align' | 'distribute' | 'flip' | 'pack' | 'resize_to_bounds' | 'stack' | 'stretch'; +} + +// @public +export interface TLShapeUtilCanBindOpts { + bindingType: string; + fromShape: { + type: TLShape['type']; + } | TLShape; + // @deprecated + fromShapeType: TLShape['type']; + toShape: { + type: TLShape['type']; + } | TLShape; + // @deprecated + toShapeType: TLShape['type']; +} + +// @public (undocumented) +export interface TLShapeUtilCanvasSvgDef { + // (undocumented) + component: React.ComponentType; + // (undocumented) + key: string; +} + +// @public (undocumented) +export interface TLShapeUtilConstructor = ShapeUtil> { + // (undocumented) + new (editor: Editor): U; + // (undocumented) + handledAssetTypes?: readonly string[]; + // (undocumented) + migrations?: LegacyMigrations | MigrationSequence | TLPropsMigrations; + // (undocumented) + props?: RecordProps; + // (undocumented) + type: T['type']; +} + +// @public (undocumented) +export interface TLShapeWrapperProps extends React.HTMLAttributes { + children: ReactNode; + isBackground: boolean; + shape: TLShape; +} + +// @public (undocumented) +export type TLSpinnerProps = React.SVGProps; + +// @public (undocumented) +export interface TLStateNodeConstructor { + // (undocumented) + new (editor: Editor, parent?: StateNode): StateNode; + // (undocumented) + children?(): TLStateNodeConstructor[]; + // (undocumented) + id: string; + // (undocumented) + initial?: string; + // (undocumented) + isLockable: boolean; + // (undocumented) + trackPerformance: boolean; + // (undocumented) + useCoalescedEvents: boolean; +} + +// @public (undocumented) +export interface TLStoreBaseOptions { + assets?: TLAssetStore; + defaultName?: string; + initialData?: SerializedStore; + onMount?(editor: Editor): (() => void) | void; + snapshot?: Partial | TLStoreSnapshot; + themes?: Partial; + users?: TLUserStore; +} + +// @public (undocumented) +export type TLStoreEventInfo = HistoryEntry; + +// @public (undocumented) +export type TLStoreOptions = TLStoreBaseOptions & { + collaboration?: { + mode?: null | Signal<'readonly' | 'readwrite'>; + status: null | Signal<'offline' | 'online'>; + }; + id?: string; +} & TLStoreSchemaOptions; + +// @public (undocumented) +export type TLStoreSchemaOptions = { + assetUtils?: readonly TLAnyAssetUtilConstructor[]; + bindingUtils?: readonly TLAnyBindingUtilConstructor[]; + migrations?: readonly MigrationSequence[]; + records?: Record; + shapeUtils?: readonly TLAnyShapeUtilConstructor[]; +} | { + schema?: StoreSchema; +}; + +// @public (undocumented) +export type TLStoreWithStatus = { + readonly connectionStatus: 'offline' | 'online'; + readonly error?: undefined; + readonly status: 'synced-remote'; + readonly store: TLStore; +} | { + readonly error: Error; + readonly status: 'error'; + readonly store?: undefined; +} | { + readonly error?: undefined; + readonly status: 'loading'; + readonly store?: undefined; +} | { + readonly error?: undefined; + readonly status: 'not-synced'; + readonly store: TLStore; +} | { + readonly error?: undefined; + readonly status: 'synced-local'; + readonly store: TLStore; +}; + +// @public (undocumented) +export interface TLSvgExportOptions { + background?: boolean; + bounds?: Box; + darkMode?: boolean; + padding?: 'auto' | number; + pixelRatio?: number; + preserveAspectRatio?: React.SVGAttributes['preserveAspectRatio']; + scale?: number; +} + +// @public (undocumented) +export interface TLSvgTextExternalContent extends TLBaseExternalContent { + // (undocumented) + text: string; + // (undocumented) + type: 'svg-text'; +} + +// @public (undocumented) +export interface TLTextExternalContent extends TLBaseExternalContent { + // (undocumented) + html?: string; + // (undocumented) + text: string; + // (undocumented) + type: 'text'; +} + +// @public (undocumented) +export interface TLTextExternalContentSource { + // (undocumented) + data: string; + // (undocumented) + subtype: 'html' | 'json' | 'text' | 'url'; + // (undocumented) + type: 'text'; +} + +// @public (undocumented) +export interface TLTextOptions { + // (undocumented) + addFontsFromNode?: RichTextFontVisitor; + // (undocumented) + tipTapConfig?: EditorProviderProps_2; +} + +// @public (undocumented) +export type TLTickEvent = (info: TLTickEventInfo) => void; + +// @public (undocumented) +export interface TLTickEventInfo { + // (undocumented) + elapsed: number; + // (undocumented) + name: 'tick'; + // (undocumented) + type: 'misc'; +} + +// @public +export const tltime: Timers; + +// @public (undocumented) +export interface TLTldrawExternalContent extends TLBaseExternalContent { + // (undocumented) + content: TLContent; + // (undocumented) + type: 'tldraw'; +} + +// @public (undocumented) +export interface TLTldrawExternalContentSource { + // (undocumented) + data: TLContent; + // (undocumented) + type: 'tldraw'; +} + +// @public +export interface TLUndoRedoPerfEvent { + redoDepth: number; + type: 'redo' | 'undo'; + undoDepth: number; +} + +// @public (undocumented) +export interface TLUpdatePointerOptions { + // (undocumented) + accelKey?: boolean; + // (undocumented) + altKey?: boolean; + // (undocumented) + button?: number; + // (undocumented) + ctrlKey?: boolean; + immediate?: boolean; + // (undocumented) + isPen?: boolean; + // (undocumented) + metaKey?: boolean; + point?: VecLike; + // (undocumented) + pointerId?: number; + // (undocumented) + shiftKey?: boolean; +} + +// @public (undocumented) +export interface TLUrlExternalAsset { + // (undocumented) + type: 'url'; + // (undocumented) + url: string; +} + +// @public (undocumented) +export interface TLUrlExternalContent extends TLBaseExternalContent { + shapeId?: TLShapeId; + // (undocumented) + type: 'url'; + // (undocumented) + url: string; +} + +// @public +export interface TLUserPreferences { + // (undocumented) + animationSpeed?: null | number; + // (undocumented) + areKeyboardShortcutsEnabled?: boolean | null; + // (undocumented) + color?: null | string; + // (undocumented) + colorScheme?: 'dark' | 'light' | 'system'; + // (undocumented) + edgeScrollSpeed?: null | number; + // (undocumented) + enhancedA11yMode?: boolean | null; + // (undocumented) + id: string; + // (undocumented) + inputMode?: 'mouse' | 'trackpad' | null; + // (undocumented) + isDynamicSizeMode?: boolean | null; + // (undocumented) + isPasteAtCursorMode?: boolean | null; + // (undocumented) + isSnapMode?: boolean | null; + // (undocumented) + isWrapMode?: boolean | null; + // (undocumented) + isZoomDirectionInverted?: boolean | null; + // (undocumented) + locale?: null | string; + // (undocumented) + name?: null | string; +} + +// @public (undocumented) +export type TLWheelEvent = (info: TLWheelEventInfo) => void; + +// @public (undocumented) +export type TLWheelEventInfo = TLBaseEventInfo & { + delta: VecModel; + name: 'wheel'; + point: VecModel; + type: 'wheel'; +}; + +// @public +export function toDomPrecision(v: number): number; + +// @public (undocumented) +export function toFixed(v: number): number; + +// @public +export function toPrecision(n: number, precision?: number): number; + +// @public (undocumented) +export class TransformedGeometry2d extends Geometry2d { + constructor(geometry: Geometry2d, matrix: MatModel, opts?: TransformedGeometry2dOptions); + // (undocumented) + distanceToLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters): number; + // (undocumented) + distanceToPoint(point: VecLike, hitInside?: boolean, filters?: Geometry2dFilters): number; + // (undocumented) + getBoundsVertices(): Vec[]; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(filters: Geometry2dFilters): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, distance?: number, filters?: Geometry2dFilters): boolean; + // (undocumented) + hitTestPoint(point: VecLike, margin?: number, hitInside?: boolean, filters?: Geometry2dFilters): boolean; + // (undocumented) + ignoreHit(point: VecLike): boolean; + // (undocumented) + intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters): Vec[]; + // (undocumented) + intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters): Vec[]; + // (undocumented) + intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters): VecLike[]; + // (undocumented) + nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec; + // (undocumented) + transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d; +} + +// @public (undocumented) +export interface TransformedGeometry2dOptions { + // (undocumented) + debugColor?: string; + // (undocumented) + excludeFromShapeBounds?: boolean; + // (undocumented) + ignore?: boolean; + // (undocumented) + isEmptyLabel?: boolean; + // (undocumented) + isInternal?: boolean; + // (undocumented) + isLabel?: boolean; +} + +// @public (undocumented) +export type UiEvent = TLCancelEvent | TLClickEvent | TLCompleteEvent | TLKeyboardEvent | TLPinchEvent | TLPointerEvent; + +// @public (undocumented) +export type UiEventType = 'click' | 'keyboard' | 'pinch' | 'pointer' | 'wheel' | 'zoom'; + +// @public (undocumented) +export function uniq(array: { + readonly [n: number]: T; + readonly length: number; +} | null | undefined): T[]; + +// @public (undocumented) +export function useColorMode(): 'dark' | 'light'; + +// @public (undocumented) +export function useContainer(): HTMLElement; + +// @public (undocumented) +export function useContainerIfExists(): HTMLElement | null; + +// @public +export function useDelaySvgExport(): () => void; + +// @public (undocumented) +export function useEditor(): Editor; + +// @public (undocumented) +export function useEditorComponents(): Required; + +// @public +export function useEditorPortalHost(): HTMLElement | null; + +// @internal +export function useEvent, Result>(handler: (...args: Args) => Result): (...args: Args) => Result; + +// @public (undocumented) +export function useGlobalMenuIsOpen(id: string, onChange?: (isOpen: boolean) => void, onEvent?: (id: string) => void): readonly [boolean, (isOpen: boolean) => void]; + +// @public (undocumented) +export function useIsCropping(shapeId: TLShapeId): boolean; + +// @public (undocumented) +export function useIsEditing(shapeId: TLShapeId): boolean; + +// @internal (undocumented) +export function useLicenseContext(): LicenseManager; + +// @internal +export function useLicenseFeatureFlag(licenseManager: LicenseManager | null, feature: LicenseFeatureName): boolean; + +// @internal (undocumented) +export function useLocalStore(options: { + persistenceKey?: string; + sessionId?: string; + snapshot?: TLEditorSnapshot | TLStoreSnapshot; +} & TLStoreOptions): TLStoreWithStatus; + +// @public (undocumented) +export function useMaybeEditor(): Editor | null; + +// @internal +export function useMaybeLicenseManager(): LicenseManager | null; + +// @internal (undocumented) +export function useOnMount(onMount?: TLOnMountHandler): void; + +// @public (undocumented) +export function usePassThroughWheelEvents(ref: RefObject): void; + +// @public +export function usePeerIds(): TLUserId[]; + +// @public (undocumented) +export function usePresence(userId: TLUserId): null | TLInstancePresence; + +// @internal (undocumented) +export const USER_COLORS: readonly ['#FF802B', '#EC5E41', '#F2555A', '#F04F88', '#E34BA9', '#BD54C6', '#9D5BD2', '#7B66DC', '#02B1CC', '#11B3A3', '#39B178', '#55B467']; + +// @internal +export function useReactiveEvent, Result>(handler: (...args: Args) => Result): (...args: Args) => Result; + +// @internal +export function useRefState(initialValue: T): [T, Dispatch>]; + +// @public (undocumented) +export class UserPreferencesManager { + constructor(user: TLCurrentUser, colorScheme: 'dark' | 'light' | 'system'); + // (undocumented) + disposables: Set<() => void>; + // (undocumented) + dispose(): void; + // (undocumented) + getAnimationSpeed(): number; + // (undocumented) + getAreKeyboardShortcutsEnabled(): boolean; + // (undocumented) + getColor(): string; + getEdgeScrollSpeed(): number; + // (undocumented) + getEnhancedA11yMode(): boolean; + getExternalId(): string; + // @deprecated (undocumented) + getId(): string; + // (undocumented) + getInputMode(): "mouse" | "trackpad" | null; + // (undocumented) + getIsDarkMode(): boolean; + // (undocumented) + getIsDynamicResizeMode(): boolean; + // (undocumented) + getIsPasteAtCursorMode(): boolean; + // (undocumented) + getIsSnapMode(): boolean; + // (undocumented) + getIsWrapMode(): boolean; + // (undocumented) + getIsZoomDirectionInverted(): boolean; + // (undocumented) + getLocale(): string; + // (undocumented) + getName(): string; + getRecordId(): TLUserId; + // (undocumented) + getUserPreferences(): { + animationSpeed: number; + areKeyboardShortcutsEnabled: boolean; + color: string; + colorScheme: "dark" | "light" | "system" | undefined; + enhancedA11yMode: boolean; + id: string; + inputMode: "mouse" | "trackpad" | null; + isDarkMode: boolean; + isDynamicResizeMode: boolean; + isSnapMode: boolean; + isWrapMode: boolean; + isZoomDirectionInverted: boolean; + locale: string; + name: string; + }; + // (undocumented) + systemColorScheme: Atom<"dark" | "light", unknown>; + // (undocumented) + updateUserPreferences(userPreferences: Partial): void; +} + +// @public (undocumented) +export const userTypeValidator: T.Validator; + +// @internal (undocumented) +export function useShallowArrayIdentity(arr: T): T; + +// @internal (undocumented) +export function useShallowObjectIdentity(obj: T): T; + +// @public +export function useSharedSafeId(id: string): SafeId; + +// @public +export function useSvgExportContext(): null | SvgExportContext; + +// @public (undocumented) +export function useTldrawCurrentUser(opts: { + setUserPreferences?: (userPreferences: TLUserPreferences) => void; + userPreferences?: Signal | TLUserPreferences; +}): TLCurrentUser; + +// @public (undocumented) +export function useTLSchemaFromUtils(opts: TLStoreSchemaOptions): StoreSchema; + +// @public (undocumented) +export function useTLStore(opts: TLStoreOptions): TLStore; + +// @public +export function useTransform(ref: React.RefObject, x?: number, y?: number, scale?: number, rotate?: number, additionalOffset?: VecLike): void; + +// @public +export function useUniqueSafeId(suffix?: string): SafeId; + +// @public @deprecated +export function useViewportHeight(): number; + +// @internal (undocumented) +export interface ValidLicenseKeyResult { + // (undocumented) + daysSinceExpiry: number; + // (undocumented) + expiryDate: Date; + // (undocumented) + isAnnualLicense: boolean; + // (undocumented) + isAnnualLicenseExpired: boolean; + // (undocumented) + isCollaborationEnabled: boolean; + // (undocumented) + isCommentingEnabled: boolean; + // (undocumented) + isDevelopment: boolean; + // (undocumented) + isDomainValid: boolean; + // (undocumented) + isEvaluationLicense: boolean; + // (undocumented) + isEvaluationLicenseExpired: boolean; + // (undocumented) + isInternalLicense: boolean; + // (undocumented) + isLicensedWithWatermark: boolean; + // (undocumented) + isLicenseParseable: true; + // (undocumented) + isNativeLicense: boolean; + // (undocumented) + isPerpetualLicense: boolean; + // (undocumented) + isPerpetualLicenseExpired: boolean; + // (undocumented) + license: LicenseInfo; +} + +// @public (undocumented) +export class Vec { + constructor(x?: number, y?: number, z?: number); + // (undocumented) + static Abs(A: VecLike): Vec; + // (undocumented) + abs(): this; + // (undocumented) + static Add(A: VecLike, B: VecLike): Vec; + // (undocumented) + add(V: VecLike): this; + // (undocumented) + static AddScalar(A: VecLike, n: number): Vec; + // (undocumented) + addScalar(n: number): this; + // (undocumented) + static AddXY(A: VecLike, x: number, y: number): Vec; + // (undocumented) + addXY(x: number, y: number): this; + static Angle(A: VecLike, B: VecLike): number; + // (undocumented) + angle(B: VecLike): number; + static AngleBetween(A: VecLike, B: VecLike): number; + // (undocumented) + static Average(arr: VecLike[]): Vec; + // (undocumented) + static Cast(A: VecLike): Vec; + // (undocumented) + static Clamp(A: Vec, min: number, max?: number): Vec; + // (undocumented) + clamp(min: number, max?: number): this; + // (undocumented) + static Clockwise(A: VecLike, B: VecLike, C: VecLike): boolean; + // (undocumented) + clone(): Vec; + static Cpr(A: VecLike, B: VecLike): number; + // (undocumented) + cpr(V: VecLike): number; + // (undocumented) + static Cross(A: VecLike, V: VecLike): Vec; + // (undocumented) + cross(V: VecLike): this; + // (undocumented) + static Dist(A: VecLike, B: VecLike): number; + // (undocumented) + dist(V: VecLike): number; + // (undocumented) + static Dist2(A: VecLike, B: VecLike): number; + // (undocumented) + static DistanceToLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp?: boolean): number; + // (undocumented) + distanceToLineSegment(A: VecLike, B: VecLike): number; + // (undocumented) + static DistanceToLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): number; + // (undocumented) + static DistMin(A: VecLike, B: VecLike, n: number): boolean; + // (undocumented) + static Div(A: VecLike, t: number): Vec; + // (undocumented) + div(t: number): this; + // (undocumented) + static DivV(A: VecLike, B: VecLike): Vec; + // (undocumented) + divV(V: VecLike): this; + static Dpr(A: VecLike, B: VecLike): number; + // (undocumented) + dpr(V: VecLike): number; + // (undocumented) + static Equals(A: VecLike, B: VecLike): boolean; + // (undocumented) + equals(B: VecLike): boolean; + // (undocumented) + static EqualsXY(A: VecLike, x: number, y: number): boolean; + // (undocumented) + equalsXY(x: number, y: number): boolean; + // (undocumented) + static From({ x, y, z }: VecModel): Vec; + // (undocumented) + static FromAngle(r: number, length?: number): Vec; + // (undocumented) + static FromArray(v: number[]): Vec; + // (undocumented) + static IsFinite(A: VecLike): boolean; + // (undocumented) + static IsNaN(A: VecLike): boolean; + // (undocumented) + static Len(A: VecLike): number; + // (undocumented) + len(): number; + // (undocumented) + static Len2(A: VecLike): number; + // (undocumented) + len2(): number; + static Lrp(A: VecLike, B: VecLike, t: number): Vec; + // (undocumented) + lrp(B: VecLike, t: number): Vec; + // (undocumented) + static ManhattanDist(A: VecLike, B: VecLike): number; + // (undocumented) + static Max(A: VecLike, B: VecLike): Vec; + // (undocumented) + static Med(A: VecLike, B: VecLike): Vec; + // (undocumented) + static Min(A: VecLike, B: VecLike): Vec; + // (undocumented) + static Mul(A: VecLike, t: number): Vec; + // (undocumented) + mul(t: number): this; + // (undocumented) + static MulV(A: VecLike, B: VecLike): Vec; + // (undocumented) + mulV(V: VecLike): this; + // (undocumented) + static NearestPointOnLineSegment(A: VecLike, B: VecLike, P: VecLike, clamp?: boolean): Vec; + static NearestPointOnLineThroughPoint(A: VecLike, u: VecLike, P: VecLike): Vec; + // (undocumented) + static Neg(A: VecLike): Vec; + // (undocumented) + neg(): this; + // (undocumented) + static Nudge(A: VecLike, B: VecLike, distance: number): Vec; + // (undocumented) + nudge(B: VecLike, distance: number): this; + static Per(A: VecLike): Vec; + // (undocumented) + per(): this; + static PointsBetween(A: VecModel, B: VecModel, steps?: number, ease?: (t: number) => number): Vec[]; + // (undocumented) + get pressure(): number; + static Pry(A: VecLike, B: VecLike): number; + // (undocumented) + pry(V: VecLike): number; + // (undocumented) + static Rescale(A: VecLike, n: number): Vec; + // (undocumented) + static Rot(A: VecLike, r?: number): Vec; + // (undocumented) + rot(r: number): this; + // (undocumented) + static RotWith(A: VecLike, C: VecLike, r: number): Vec; + // (undocumented) + rotWith(C: VecLike, r: number): this; + // (undocumented) + static ScaleWithOrigin(A: VecLike, scale: number, origin: VecLike): Vec; + // (undocumented) + set(x?: number, y?: number, z?: number): this; + // (undocumented) + setTo({ x, y, z }: VecLike): this; + // (undocumented) + static Slope(A: VecLike, B: VecLike): number; + // (undocumented) + slope(B: VecLike): number; + // (undocumented) + static Snap(A: VecLike, step?: number): Vec; + // (undocumented) + static SnapToGrid(A: VecLike, gridSize?: number): Vec; + // (undocumented) + snapToGrid(gridSize: number): this; + // (undocumented) + static Sub(A: VecLike, B: VecLike): Vec; + // (undocumented) + sub(V: VecLike): this; + // (undocumented) + static SubScalar(A: VecLike, n: number): Vec; + // (undocumented) + subScalar(n: number): this; + // (undocumented) + static SubXY(A: VecLike, x: number, y: number): Vec; + // (undocumented) + subXY(x: number, y: number): this; + // (undocumented) + static Tan(A: VecLike, B: VecLike): Vec; + // (undocumented) + tan(V: VecLike): Vec; + // (undocumented) + static ToAngle(A: VecLike): number; + // (undocumented) + toAngle(): number; + // (undocumented) + static ToArray(A: VecLike): number[]; + // (undocumented) + toArray(): number[]; + // (undocumented) + static ToCss(A: VecLike): string; + // (undocumented) + static ToFixed(A: VecLike): Vec; + // (undocumented) + toFixed(): this; + // (undocumented) + static ToInt(A: VecLike): Vec; + // (undocumented) + static ToJson(A: VecLike): { + x: number; + y: number; + z: number | undefined; + }; + // (undocumented) + toJson(): VecModel; + // (undocumented) + static ToString(A: VecLike): string; + // (undocumented) + toString(): string; + static Uni(A: VecLike): Vec; + // (undocumented) + uni(): this; + // (undocumented) + x: number; + // (undocumented) + y: number; + // (undocumented) + z: number; +} + +// @public (undocumented) +export type VecLike = Vec | VecModel; + + +export * from "@tldraw/state"; +export * from "@tldraw/state-react"; +export * from "@tldraw/store"; +export * from "@tldraw/tlschema"; +export * from "@tldraw/utils"; +export * from "@tldraw/validate"; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts new file mode 100644 index 0000000..7bded41 --- /dev/null +++ b/packages/editor/src/lib/editor/Editor.ts @@ -0,0 +1,11735 @@ +import { + Atom, + EMPTY_ARRAY, + atom, + computed, + react, + transact, + unsafe__withoutCapture, +} from '@tldraw/state' +import { + ComputedCache, + RecordType, + StoreSideEffects, + StoreSnapshot, + UnknownRecord, + reverseRecordsDiff, +} from '@tldraw/store' +import { + CameraRecordType, + InstancePageStateRecordType, + PageRecordType, + StyleProp, + StylePropValue, + TLAsset, + TLAssetId, + TLAssetPartial, + TLBinding, + TLBindingCreate, + TLBindingId, + TLBindingUpdate, + TLCamera, + TLCreateShapePartial, + TLCursor, + TLCursorType, + TLDOCUMENT_ID, + TLDocument, + TLGroupShape, + TLHandle, + TLINSTANCE_ID, + TLImageAsset, + TLInstance, + TLInstancePageState, + TLInstancePresence, + TLPage, + TLPageId, + TLParentId, + TLRecord, + TLShape, + TLShapeId, + TLShapePartial, + TLStore, + TLStoreSnapshot, + TLTheme, + TLThemeId, + TLThemes, + TLUser, + TLUserId, + TLVideoAsset, + UserRecordType, + createBindingId, + createShapeId, + createUserId, + getShapePropKeysByStyle, + isPageId, + isShapeId, +} from '@tldraw/tlschema' +import { + FileHelpers, + IndexKey, + JsonObject, + PerformanceTracker, + Result, + ZERO_INDEX_KEY, + annotateError, + areArraysShallowEqual, + assert, + assertExists, + bind, + compact, + debounce, + dedupe, + exhaustiveSwitchError, + fetch, + getIndexAbove, + getIndexBetween, + getIndices, + getIndicesAbove, + getIndicesBetween, + getOwnProperty, + hasOwnProperty, + last, + lerp, + minBy, + sortById, + sortByIndex, + structuredClone, + uniqueId, +} from '@tldraw/utils' +import EventEmitter from 'eventemitter3' +import { TLCurrentUser, createTLCurrentUser } from '../config/createTLCurrentUser' +import { TLAnyAssetUtilConstructor, checkAssets } from '../config/defaultAssets' +import { TLAnyBindingUtilConstructor, checkBindings } from '../config/defaultBindings' +import { TLAnyShapeUtilConstructor, checkShapesAndAddCore } from '../config/defaultShapes' +import { + TLEditorSnapshot, + TLLoadSnapshotOptions, + getSnapshot, + loadSnapshot, +} from '../config/TLEditorSnapshot' +import { + DEFAULT_ANIMATION_OPTIONS, + DEFAULT_CAMERA_OPTIONS, + FRAME_MS_60HZ, + INTERNAL_POINTER_IDS, + LEFT_MOUSE_BUTTON, + MIDDLE_MOUSE_BUTTON, + RIGHT_MOUSE_BUTTON, + STYLUS_ERASER_BUTTON, +} from '../constants' +import { getOwnerWindow } from '../exports/domUtils' +import { exportToSvg } from '../exports/exportToSvg' +import { getSvgAsImageWithOptions, trimSvgToContent } from '../exports/getSvgAsImage' +import { registerMountedEditor, unregisterMountedEditor } from '../globals/editors' +import { tlmenus } from '../globals/menus' +import { tltime } from '../globals/time' +import { LicenseManager } from '../license/LicenseManager' +import { TldrawOptions, defaultTldrawOptions } from '../options' +import { Box, BoxLike } from '../primitives/Box' +import { EASINGS } from '../primitives/easings' +import { Geometry2d } from '../primitives/geometry/Geometry2d' +import { Group2d } from '../primitives/geometry/Group2d' +import { intersectPolygonPolygon } from '../primitives/intersect' +import { Mat, MatLike } from '../primitives/Mat' +import { + HALF_PI, + approximately, + areAnglesCompatible, + clamp, + pointInPolygon, +} from '../primitives/utils' +import { Vec, VecLike } from '../primitives/Vec' +import { areShapesContentEqual } from '../utils/areShapesContentEqual' +import { dataUrlToFile } from '../utils/assets' +import { debugFlags } from '../utils/debug-flags' +import { + TLDeepLink, + TLDeepLinkOptions, + createDeepLinkString, + parseDeepLinkString, +} from '../utils/deepLinks' +import { getIncrementedName } from '../utils/getIncrementedName' +import { getReorderingShapesChanges } from '../utils/reorderShapes' +import { getDroppedShapesToNewParents, kickoutOccludedShapes } from '../utils/reparenting' +import { TLTextOptions, TiptapEditor } from '../utils/richText' +import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation' +import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap' +import { AssetUtil } from './assets/AssetUtil' +import { BindingOnDeleteOptions, BindingUtil } from './bindings/BindingUtil' +import { bindingsIndex } from './derivations/bindingsIndex' +import { notVisibleShapes } from './derivations/notVisibleShapes' +import { parentsToChildren } from './derivations/parentsToChildren' +import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage' +import { ClickManager } from './managers/ClickManager/ClickManager' +import { CollaboratorsManager } from './managers/CollaboratorsManager/CollaboratorsManager' +import { EdgeScrollManager } from './managers/EdgeScrollManager/EdgeScrollManager' +import { FocusManager } from './managers/FocusManager/FocusManager' +import { FontManager } from './managers/FontManager/FontManager' +import { HistoryManager } from './managers/HistoryManager/HistoryManager' +import { InputsManager } from './managers/InputsManager/InputsManager' +import { PerformanceManager } from './managers/PerformanceManager/PerformanceManager' +import { ScribbleManager } from './managers/ScribbleManager/ScribbleManager' +import { SnapManager } from './managers/SnapManager/SnapManager' +import { SpatialIndexManager } from './managers/SpatialIndexManager/SpatialIndexManager' +import { TextManager } from './managers/TextManager/TextManager' +import { ThemeManager, resolveThemes } from './managers/ThemeManager/ThemeManager' +import { TickManager } from './managers/TickManager/TickManager' +import { UserPreferencesManager } from './managers/UserPreferencesManager/UserPreferencesManager' +import { OverlayManager } from './overlays/OverlayManager' +import { TLAnyOverlayUtilConstructor } from './overlays/OverlayUtil' +import { + ShapeUtil, + TLEditStartInfo, + TLGeometryOpts, + TLResizeMode, + TLShapeUtilCanBeLaidOutOpts, + TLShapeUtilCanBindOpts, +} from './shapes/ShapeUtil' +import { RootState } from './tools/RootState' +import { StateNode, TLStateNodeConstructor } from './tools/StateNode' +import { TLContent } from './types/clipboard-types' +import { TLEventMap } from './types/emit-types' +import { TLEventInfo, TLPointerEventInfo } from './types/event-types' +import { TLExternalAsset, TLExternalContent } from './types/external-content' +import { TLHistoryBatchOptions } from './types/history-types' +import { + OptionalKeys, + RequiredKeys, + TLCameraConstraints, + TLCameraMoveOptions, + TLCameraOptions, + TLGetShapeAtPointOptions, + TLImageExportOptions, + TLSvgExportOptions, + TLUpdatePointerOptions, +} from './types/misc-types' +import { TLAdjacentDirection, TLResizeHandle } from './types/selection-types' + +/** @public */ +export type TLResizeShapeOptions = Partial<{ + initialBounds: Box + scaleOrigin: VecLike + scaleAxisRotation: number + initialShape: TLShape + initialPageTransform: MatLike + dragHandle: TLResizeHandle + isAspectRatioLocked: boolean + mode: TLResizeMode + skipStartAndEndCallbacks: boolean +}> + +/** @public */ +export interface TLEditorOptions { + /** + * The Store instance to use for keeping the editor's data. This may be prepopulated, e.g. by loading + * from a server or database. + */ + store: TLStore + /** + * An array of shapes to use in the editor. These will be used to create and manage shapes in the editor. + */ + shapeUtils: readonly TLAnyShapeUtilConstructor[] + /** + * An array of bindings to use in the editor. These will be used to create and manage bindings in the editor. + */ + bindingUtils: readonly TLAnyBindingUtilConstructor[] + /** + * An array of asset utils to use in the editor. These will be used to handle asset-type-specific behavior. + */ + assetUtils?: readonly TLAnyAssetUtilConstructor[] + /** + * An array of overlay utils to use in the editor. These define canvas overlay UI elements + * like selection handles, rotation corners, shape handles, etc. + */ + overlayUtils?: readonly TLAnyOverlayUtilConstructor[] + /** + * An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor. + */ + tools: readonly TLStateNodeConstructor[] + /** + * A user defined externally to replace the default user. + */ + user?: TLCurrentUser + /** + * The editor's initial active tool (or other state node id). + */ + initialState?: string + /** + * Whether to automatically focus the editor when it mounts. + */ + autoFocus?: boolean + licenseKey?: string + fontAssetUrls?: { [key: string]: string | undefined } + /** + * Should return a containing html element which has all the styles applied to the editor. If not + * given, the body element will be used. + */ + getContainer(): HTMLElement + /** + * Provides a way to hide shapes. + * + * @example + * ```ts + * getShapeVisibility={(shape, editor) => shape.meta.hidden ? 'hidden' : 'inherit'} + * ``` + * + * - `'inherit' | undefined` - (default) The shape will be visible unless its parent is hidden. + * - `'hidden'` - The shape will be hidden. + * - `'visible'` - The shape will be visible. + * + * @param shape - The shape to check. + * @param editor - The editor instance. + */ + getShapeVisibility?( + shape: TLShape, + editor: Editor + ): 'visible' | 'hidden' | 'inherit' | null | undefined + /** + * Named theme definitions for the editor. Each theme contains shared + * properties (font size, line height, stroke width) and color palettes + * for both light and dark modes. + */ + themes?: Partial + /** + * The id of the initially active theme. Defaults to `'default'`. + */ + initialTheme?: TLThemeId + /** + * The editor's color scheme preference, controls the default color mode. Defaults to `'light'`. + * + * - `'light'` - Always use light mode. + * - `'dark'` - Always use dark mode. + * - `'system'` - Follow the OS color scheme preference. + */ + colorScheme?: 'light' | 'dark' | 'system' + /** + * Additional configuration options for the tldraw editor. + */ + options?: Partial + // --- Deprecated ---- + /** + * Options for the editor's camera. + * + * @deprecated Use `options.cameraOptions` instead. This will be removed in a future release. + */ + cameraOptions?: Partial + /** + * Text options for the editor. + * + * @deprecated Use `options.text` instead. This prop will be removed in a future release. + */ + textOptions?: TLTextOptions +} + +/** + * Options for {@link Editor.(run:1)}. + * @public + */ +export interface TLEditorRunOptions extends TLHistoryBatchOptions { + ignoreShapeLock?: boolean +} + +/** @public */ +export interface TLRenderingShape { + id: TLShapeId + shape: TLShape + util: ShapeUtil + index: number + backgroundIndex: number + opacity: number +} + +const RENDERING_SHAPES_SORT_CACHE_THRESHOLD = 100 + +const AXIS = { + horizontal: { val: 'x', min: 'minX', max: 'maxX', dim: 'width' }, + vertical: { val: 'y', min: 'minY', max: 'maxY', dim: 'height' }, +} as const + +/** @public */ +export class Editor extends EventEmitter { + readonly id = uniqueId() + constructor({ + store, + user, + shapeUtils, + bindingUtils, + assetUtils: assetUtilConstructors, + overlayUtils: overlayUtilConstructors, + tools, + getContainer, + // needs to be here for backwards compatibility with TldrawEditor + // eslint-disable-next-line @typescript-eslint/no-deprecated + cameraOptions, + initialState, + autoFocus, + options: _options, + // needs to be here for backwards compatibility with TldrawEditor + // eslint-disable-next-line @typescript-eslint/no-deprecated + textOptions: _textOptions, + getShapeVisibility, + colorScheme, + fontAssetUrls, + themes, + initialTheme, + }: TLEditorOptions) { + super() + + this._getShapeVisibility = getShapeVisibility + + // Merge deprecated textOptions prop with options.text + // options.text takes precedence over the deprecated textOptions prop + const options = _textOptions ? { ..._options, text: _options?.text ?? _textOptions } : _options + + this.options = { ...defaultTldrawOptions, ...options } + + this.store = store + this.history = new HistoryManager({ + store, + annotateError: (error: any) => { + this.annotateError(error, { origin: 'history.batch', willCrashApp: true }) + this.crash(error) + }, + }) + + this.snaps = new SnapManager(this) + + this._spatialIndex = new SpatialIndexManager(this) + this.disposables.add(() => this._spatialIndex.dispose()) + + this.disposables.add(this.timers.dispose) + + // Merge camera options: options.cameraOptions takes precedence over deprecated cameraOptions prop + this._cameraOptions.set({ + ...DEFAULT_CAMERA_OPTIONS, + ...cameraOptions, + ...options?.camera, + }) + + this.getContainer = getContainer + + this._textOptions = atom('text options', options?.text ?? null) + + this.user = new UserPreferencesManager(user ?? createTLCurrentUser(), colorScheme ?? 'light') + this.disposables.add(() => this.user.dispose()) + + this.textMeasure = new TextManager(this) + this.disposables.add(() => this.textMeasure.dispose()) + + this._themeManager = new ThemeManager(this, { + themes: resolveThemes(themes), + initial: initialTheme ?? 'default', + }) + this.disposables.add(() => this._themeManager.dispose()) + + this._tickManager = new TickManager(this) + this.disposables.add(() => this._tickManager.dispose()) + this.disposables.add(() => { + this._setCameraState('idle') + }) + + this.fonts = new FontManager(this, fontAssetUrls) + this.disposables.add(() => this.fonts.dispose()) + + this.inputs = new InputsManager(this) + this.disposables.add(() => this.inputs.dispose()) + this.performance = new PerformanceManager(this) + this.disposables.add(() => this.performance.dispose()) + this.collaborators = new CollaboratorsManager(this) + + class NewRoot extends RootState { + static override initial = initialState ?? '' + } + + this.root = new NewRoot(this) + this.root.children = {} + + this.markEventAsHandled = this.markEventAsHandled.bind(this) + + const allShapeUtils = checkShapesAndAddCore(shapeUtils) + + const _shapeUtils = {} as Record> + const _styleProps = {} as Record, string>> + const allStylesById = new Map>() + + for (const Util of allShapeUtils) { + const util = new Util(this) + _shapeUtils[Util.type] = util + + const propKeysByStyle = getShapePropKeysByStyle(Util.props ?? {}) + _styleProps[Util.type] = propKeysByStyle + + for (const style of propKeysByStyle.keys()) { + if (!allStylesById.has(style.id)) { + allStylesById.set(style.id, style) + } else if (allStylesById.get(style.id) !== style) { + throw Error( + `Multiple style props with id "${style.id}" in use. Style prop IDs must be unique.` + ) + } + } + } + + this.shapeUtils = _shapeUtils + this.styleProps = _styleProps + + const _shapeUtilsByAssetType = {} as Record> + for (const Util of allShapeUtils) { + const assetTypes = Util.handledAssetTypes + if (assetTypes) { + for (const assetType of assetTypes) { + _shapeUtilsByAssetType[assetType] = _shapeUtils[Util.type] + } + } + } + this._shapeUtilsByAssetType = _shapeUtilsByAssetType + + const allBindingUtils = checkBindings(bindingUtils) + const _bindingUtils = {} as Record> + for (const Util of allBindingUtils) { + const util = new Util(this) + _bindingUtils[Util.type] = util + } + this.bindingUtils = _bindingUtils + + // Asset utils + if (assetUtilConstructors) { + const allAssetUtils = checkAssets(assetUtilConstructors) + const _assetUtils = {} as Record> + for (const Util of allAssetUtils) { + const util = new Util(this) + _assetUtils[Util.type] = util + } + this.assetUtils = _assetUtils + } + + // Tools. + // Accept tools from constructor parameters which may not conflict with the root note's default or + // "baked in" tools, select and zoom. + for (const Tool of [...tools]) { + if (hasOwnProperty(this.root.children!, Tool.id)) { + throw Error(`Can't override tool with id "${Tool.id}"`) + } + this.root.children![Tool.id] = new Tool(this, this.root) + } + + this.scribbles = new ScribbleManager(this) + + // Overlay utils + this.overlays = new OverlayManager(this) + this.disposables.add(() => this.overlays.dispose()) + if (overlayUtilConstructors) { + for (const Util of overlayUtilConstructors) { + const util = new Util(this) + this.overlays.registerUtil(util) + } + } + + // Cleanup + + const cleanupInstancePageState = ( + prevPageState: TLInstancePageState, + shapesNoLongerInPage: Set + ) => { + let nextPageState = null as null | TLInstancePageState + + const selectedShapeIds = prevPageState.selectedShapeIds.filter( + (id) => !shapesNoLongerInPage.has(id) + ) + if (selectedShapeIds.length !== prevPageState.selectedShapeIds.length) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.selectedShapeIds = selectedShapeIds + } + + const erasingShapeIds = prevPageState.erasingShapeIds.filter( + (id) => !shapesNoLongerInPage.has(id) + ) + if (erasingShapeIds.length !== prevPageState.erasingShapeIds.length) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.erasingShapeIds = erasingShapeIds + } + + if (prevPageState.hoveredShapeId && shapesNoLongerInPage.has(prevPageState.hoveredShapeId)) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.hoveredShapeId = null + } + + if (prevPageState.editingShapeId && shapesNoLongerInPage.has(prevPageState.editingShapeId)) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.editingShapeId = null + } + + if ( + prevPageState.croppingShapeId && + shapesNoLongerInPage.has(prevPageState.croppingShapeId) + ) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.croppingShapeId = null + } + + const hintingShapeIds = prevPageState.hintingShapeIds.filter( + (id) => !shapesNoLongerInPage.has(id) + ) + if (hintingShapeIds.length !== prevPageState.hintingShapeIds.length) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.hintingShapeIds = hintingShapeIds + } + + if (prevPageState.focusedGroupId && shapesNoLongerInPage.has(prevPageState.focusedGroupId)) { + if (!nextPageState) nextPageState = { ...prevPageState } + nextPageState.focusedGroupId = null + } + return nextPageState + } + + this.sideEffects = this.store.sideEffects + + let deletedBindings = new Map>() + const deletedShapeIds = new Set() + const invalidParents = new Set() + const createdShapes = new Set() + let invalidBindingTypes = new Set() + + this.disposables.add( + this.sideEffects.registerOperationCompleteHandler(() => { + // this needs to be cleared here because further effects may delete more shapes + // and we want the next invocation of this handler to handle those separately + const deletedIds = deletedShapeIds.size ? new Set(deletedShapeIds) : null + deletedShapeIds.clear() + + if (deletedIds) { + const updates = compact( + this.getPageStates().map((pageState) => { + return cleanupInstancePageState(pageState, deletedIds) + }) + ) + + if (updates.length) { + this.store.put(updates) + } + } + + const justCreatedShapeIds = new Set(createdShapes) + createdShapes.clear() + + for (const parentId of invalidParents) { + invalidParents.delete(parentId) + if (justCreatedShapeIds.has(parentId)) continue + const parent = this.getShape(parentId) + if (!parent) continue + + const util = this.getShapeUtil(parent) + const changes = util.onChildrenChange?.(parent) + + if (changes?.length) { + this.updateShapes(changes) + } + } + + if (invalidBindingTypes.size) { + const t = invalidBindingTypes + invalidBindingTypes = new Set() + for (const type of t) { + const util = this.getBindingUtil(type) + util.onOperationComplete?.() + } + } + + if (deletedBindings.size) { + const t = deletedBindings + deletedBindings = new Map() + for (const opts of t.values()) { + this.getBindingUtil(opts.binding).onAfterDelete?.(opts) + } + } + + this.emit('update') + }) + ) + + this.disposables.add( + this.sideEffects.register({ + shape: { + afterCreate: (shape) => { + createdShapes.add(shape.id) + if (shape.parentId && isShapeId(shape.parentId)) { + invalidParents.add(shape.parentId) + } + }, + afterChange: (shapeBefore, shapeAfter) => { + for (const binding of this.getBindingsInvolvingShape(shapeAfter)) { + invalidBindingTypes.add(binding.type) + if (binding.fromId === shapeAfter.id) { + this.getBindingUtil(binding).onAfterChangeFromShape?.({ + binding, + shapeBefore, + shapeAfter, + reason: 'self', + }) + } + if (binding.toId === shapeAfter.id) { + this.getBindingUtil(binding).onAfterChangeToShape?.({ + binding, + shapeBefore, + shapeAfter, + reason: 'self', + }) + } + } + + // if the shape's parent changed and it has a binding, update the binding + if (shapeBefore.parentId !== shapeAfter.parentId) { + const notifyBindingAncestryChange = (id: TLShapeId) => { + const descendantShape = this.getShape(id) + if (!descendantShape) return + + for (const binding of this.getBindingsInvolvingShape(descendantShape)) { + invalidBindingTypes.add(binding.type) + + if (binding.fromId === descendantShape.id) { + this.getBindingUtil(binding).onAfterChangeFromShape?.({ + binding, + shapeBefore: descendantShape, + shapeAfter: descendantShape, + reason: 'ancestry', + }) + } + if (binding.toId === descendantShape.id) { + this.getBindingUtil(binding).onAfterChangeToShape?.({ + binding, + shapeBefore: descendantShape, + shapeAfter: descendantShape, + reason: 'ancestry', + }) + } + } + } + notifyBindingAncestryChange(shapeAfter.id) + this.visitDescendants(shapeAfter.id, notifyBindingAncestryChange) + } + + // if this shape moved to a new page, clean up any previous page's instance state + if (shapeBefore.parentId !== shapeAfter.parentId && isPageId(shapeAfter.parentId)) { + const allMovingIds = new Set([shapeBefore.id]) + this.visitDescendants(shapeBefore.id, (id) => { + allMovingIds.add(id) + }) + + for (const instancePageState of this.getPageStates()) { + if (instancePageState.pageId === shapeAfter.parentId) continue + const nextPageState = cleanupInstancePageState(instancePageState, allMovingIds) + + if (nextPageState) { + this.store.put([nextPageState]) + } + } + } + + if (shapeBefore.parentId && isShapeId(shapeBefore.parentId)) { + invalidParents.add(shapeBefore.parentId) + } + + if (shapeAfter.parentId !== shapeBefore.parentId && isShapeId(shapeAfter.parentId)) { + invalidParents.add(shapeAfter.parentId) + } + }, + beforeDelete: (shape) => { + // if we triggered this delete with a recursive call, don't do anything + if (deletedShapeIds.has(shape.id)) return + // if the deleted shape has a parent shape make sure we call it's onChildrenChange callback + if (shape.parentId && isShapeId(shape.parentId)) { + invalidParents.add(shape.parentId) + } + + deletedShapeIds.add(shape.id) + + const deleteBindingIds: TLBindingId[] = [] + for (const binding of this.getBindingsInvolvingShape(shape)) { + invalidBindingTypes.add(binding.type) + deleteBindingIds.push(binding.id) + const util = this.getBindingUtil(binding) + if (binding.fromId === shape.id) { + util.onBeforeIsolateToShape?.({ binding, removedShape: shape }) + util.onBeforeDeleteFromShape?.({ binding, shape }) + } else { + util.onBeforeIsolateFromShape?.({ binding, removedShape: shape }) + util.onBeforeDeleteToShape?.({ binding, shape }) + } + } + + if (deleteBindingIds.length) { + // straight to the store: this cleanup must run even when deleteBindings would + // refuse (readonly), e.g. for a deletion that arrived from a remote peer + this.store.remove(deleteBindingIds) + } + }, + }, + binding: { + beforeCreate: (binding) => { + const next = this.getBindingUtil(binding).onBeforeCreate?.({ binding }) + if (next) return next + return binding + }, + afterCreate: (binding) => { + invalidBindingTypes.add(binding.type) + this.getBindingUtil(binding).onAfterCreate?.({ binding }) + }, + beforeChange: (bindingBefore, bindingAfter) => { + const updated = this.getBindingUtil(bindingAfter).onBeforeChange?.({ + bindingBefore, + bindingAfter, + }) + if (updated) return updated + return bindingAfter + }, + afterChange: (bindingBefore, bindingAfter) => { + invalidBindingTypes.add(bindingAfter.type) + this.getBindingUtil(bindingAfter).onAfterChange?.({ bindingBefore, bindingAfter }) + }, + beforeDelete: (binding) => { + this.getBindingUtil(binding).onBeforeDelete?.({ binding }) + }, + afterDelete: (binding) => { + this.getBindingUtil(binding).onAfterDelete?.({ binding }) + invalidBindingTypes.add(binding.type) + }, + }, + page: { + afterCreate: (record) => { + const cameraId = CameraRecordType.createId(record.id) + const _pageStateId = InstancePageStateRecordType.createId(record.id) + if (!this.store.has(cameraId)) { + this.store.put([CameraRecordType.create({ id: cameraId })]) + } + if (!this.store.has(_pageStateId)) { + this.store.put([ + InstancePageStateRecordType.create({ id: _pageStateId, pageId: record.id }), + ]) + } + }, + afterDelete: (record, source) => { + // page was deleted, need to check whether it's the current page and select another one if so + if (this.getInstanceState()?.currentPageId === record.id) { + const backupPageId = this.getPages().find((p) => p.id !== record.id)?.id + if (backupPageId) { + this.store.put([{ ...this.getInstanceState(), currentPageId: backupPageId }]) + } else if (source === 'user') { + // fall back to ensureStoreIsUsable: + this.store.ensureStoreIsUsable() + } + } + + // delete the camera and state for the page if necessary + const cameraId = CameraRecordType.createId(record.id) + const instance_PageStateId = InstancePageStateRecordType.createId(record.id) + this.store.remove([cameraId, instance_PageStateId]) + }, + }, + instance: { + afterChange: (prev, next, source) => { + // instance should never be updated to a page that no longer exists (this can + // happen when undoing a change that involves switching to a page that has since + // been deleted by another user) + if (!this.store.has(next.currentPageId)) { + const backupPageId = this.store.has(prev.currentPageId) + ? prev.currentPageId + : this.getPages()[0]?.id + if (backupPageId) { + this.store.update(next.id, (instance) => ({ + ...instance, + currentPageId: backupPageId, + })) + } else if (source === 'user') { + // fall back to ensureStoreIsUsable: + this.store.ensureStoreIsUsable() + } + } + }, + }, + instance_page_state: { + afterChange: (prev, next) => { + if (prev?.focusedGroupId !== next?.focusedGroupId) { + this.cancelDoubleClick() + } + + if (prev?.selectedShapeIds !== next?.selectedShapeIds) { + // ensure that descendants and ancestors are not selected at the same time + const selectedShapeIds = new Set(next.selectedShapeIds) + const filtered = next.selectedShapeIds.filter((id) => { + let parentId = this.getShape(id)?.parentId + while (isShapeId(parentId)) { + if (selectedShapeIds.has(parentId)) { + return false + } + parentId = this.getShape(parentId)?.parentId + } + return true + }) + + let nextFocusedGroupId: null | TLShapeId = null + + if (filtered.length > 0) { + const commonGroupAncestor = this.findCommonAncestor( + compact(filtered.map((id) => this.getShape(id))), + (shape) => this.isShapeOfType(shape, 'group') + ) + + if (commonGroupAncestor) { + nextFocusedGroupId = commonGroupAncestor + } + } else { + if (next?.focusedGroupId) { + nextFocusedGroupId = next.focusedGroupId + } + } + + if ( + filtered.length !== next.selectedShapeIds.length || + nextFocusedGroupId !== next.focusedGroupId + ) { + this.store.put([ + { + ...next, + selectedShapeIds: filtered, + focusedGroupId: nextFocusedGroupId ?? null, + }, + ]) + } + } + }, + }, + }) + ) + + this._currentPageShapeIds = deriveShapeIdsInCurrentPage(this.store, () => + this.getCurrentPageId() + ) + this._parentIdsToChildIds = parentsToChildren(this.store) + + this.disposables.add( + this.store.listen((changes) => { + this.emit('change', changes) + }) + ) + this.disposables.add(this.history.dispose) + + this.run( + () => { + this.store.ensureStoreIsUsable() + + // clear ephemeral state + this._updateCurrentPageState({ + editingShapeId: null, + hoveredShapeId: null, + erasingShapeIds: [], + }) + }, + { history: 'ignore' } + ) + + if (initialState && this.root.children[initialState] === undefined) { + throw Error(`No state found for initialState "${initialState}".`) + } + + this.root.enter(undefined, 'initial') + + this.edgeScrollManager = new EdgeScrollManager(this) + this.focusManager = new FocusManager(this, autoFocus) + this.disposables.add(() => this.focusManager.dispose()) + + if (this.getInstanceState().followingUserId) { + this.stopFollowingUser() + } + + this.on('tick', this._flushEventsForTick) + + this.on('mount', () => { + this._isMounted.set(true) + registerMountedEditor(this) + }) + + this.on('unmount', () => { + this._isMounted.set(false) + unregisterMountedEditor(this) + }) + + this.timers.requestAnimationFrame(() => { + this._tickManager.start() + }) + + this.performanceTracker = new PerformanceTracker() + + if (this.store.props.collaboration?.mode) { + const mode = this.store.props.collaboration.mode + this.disposables.add( + react('update collaboration mode', () => { + const isReadonly = mode.get() === 'readonly' + // only track `mode`, and keep the sync out of the user's undo history + unsafe__withoutCapture(() => + this._updateInstanceState({ isReadonly }, { history: 'ignore' }) + ) + }) + ) + } + + this.disposables.add( + react('sync current user record', () => { + const user = this.store.props.users.currentUser.get() + if (user) { + this._ensureUserRecord(user) + } + }) + ) + } + + private readonly _getShapeVisibility?: TLEditorOptions['getShapeVisibility'] + + @computed + private getIsShapeHiddenCache() { + if (!this._getShapeVisibility) return null + return this.store.createComputedCache('isShapeHidden', (shape: TLShape) => { + const visibility = this._getShapeVisibility!(shape, this) + const isParentHidden = PageRecordType.isId(shape.parentId) + ? false + : this.isShapeHidden(shape.parentId) + + if (isParentHidden) return visibility !== 'visible' + return visibility === 'hidden' + }) + } + isShapeHidden(shapeOrId: TLShape | TLShapeId): boolean { + if (!this._getShapeVisibility) return false + return !!this.getIsShapeHiddenCache!()!.get( + typeof shapeOrId === 'string' ? shapeOrId : shapeOrId.id + ) + } + + readonly options: TldrawOptions + + /** + * The license manager whose feature flags apply to this editor. Assigned by `` + * when it creates the editor, so that UI rendered outside the editor tree can resolve the + * editor's license through the editor instance. Undefined for editors created directly. + * + * @internal + */ + licenseManager?: LicenseManager + + readonly contextId = uniqueId() + + /** + * The editor's store + * + * @public + */ + readonly store: TLStore + + /** + * The root state of the statechart. + * + * @public + */ + readonly root: StateNode + + /** + * Set a tool. Useful if you need to add a tool to the state chart on demand, + * after the editor has already been initialized. + * + * @param Tool - The tool to set. + * @param parent - The parent state node to set the tool on. + * + * @public + */ + setTool(Tool: TLStateNodeConstructor, parent?: StateNode) { + parent ??= this.root + if (hasOwnProperty(parent.children!, Tool.id)) { + throw Error(`Can't override tool with id "${Tool.id}"`) + } + parent.children![Tool.id] = new Tool(this, parent) + } + + /** + * Remove a tool. Useful if you need to remove a tool from the state chart on demand, + * after the editor has already been initialized. + * + * @param Tool - The tool to delete. + * @param parent - The parent state node to remove the tool from. + * + * @public + */ + removeTool(Tool: TLStateNodeConstructor, parent?: StateNode) { + parent ??= this.root + if (hasOwnProperty(parent.children!, Tool.id)) { + delete parent.children![Tool.id] + } + } + + /** + * A set of functions to call when the editor is disposed. + * + * @public + */ + readonly disposables = new Set<() => void>() + + /** + * Whether the editor is disposed. + * + * @public + */ + isDisposed = false + + private readonly _isMounted = atom('isMounted', false) + + /** + * Whether the editor is currently mounted. This is `true` while the editor's component is + * mounted in the DOM (after the `mount` event) and `false` before mount and after `unmount`. + * + * Unlike disposal, mounting is not terminal: the editor's component can unmount and remount + * (for example when the canvas is replaced by an error fallback and restored) without the + * editor itself being disposed. To react to the transitions, listen to the editor's `mount` + * and `unmount` events. + * + * @public + */ + @computed getIsMounted(): boolean { + return this._isMounted.get() + } + + /** + * A manager for the editor's tick events. + * + * @internal */ + private readonly _tickManager: TickManager + + /** + * A manager for the editor's input state. + * + * @public + */ + readonly inputs: InputsManager + + /** + * A manager for the editor's snapping feature. + * + * @public + */ + readonly snaps: SnapManager + + /** + * A manager for performance measurement hooks. + * + * @public + */ + readonly performance: PerformanceManager + + /** + * A manager for the spatial index, tracking where shapes exist on the canvas. + * + * @internal + */ + private readonly _spatialIndex: SpatialIndexManager + + /** + * A manager for the any asynchronous events and making sure they're + * cleaned up upon disposal. + * + * @public + */ + readonly timers = tltime.forContext(this.contextId) + + /** + * A manager for remote peer collaborators connected to this editor. + * + * @public + */ + readonly collaborators: CollaboratorsManager + + /** + * A manager for the user and their preferences. + * + * @public + */ + readonly user: UserPreferencesManager + + /** + * A manager for the editor's themes. + * + * @internal + */ + private readonly _themeManager: ThemeManager + + /** + * A helper for measuring text. + * + * @public + */ + readonly textMeasure: TextManager + + /** + * A utility for managing the set of fonts that should be rendered in the document. + * + * @public + */ + readonly fonts: FontManager + + /** + * A manager for the editor's scribbles. + * + * @public + */ + readonly scribbles: ScribbleManager + + /** + * A manager for canvas overlay UI elements (selection handles, shape handles, etc.). + * + * @public + */ + readonly overlays: OverlayManager + + /** + * A manager for side effects and correct state enforcement. See {@link @tldraw/store#StoreSideEffects} for details. + * + * @public + */ + readonly sideEffects: StoreSideEffects + + /** + * A manager for moving the camera when the mouse is at the edge of the screen. + * + * @public + */ + edgeScrollManager: EdgeScrollManager + + /** + * A manager for ensuring correct focus. See FocusManager for details. + * + * @internal + */ + private focusManager: FocusManager + + /** + * The current HTML element containing the editor. + * + * @example + * ```ts + * const container = editor.getContainer() + * ``` + * + * @public + */ + getContainer: () => HTMLElement + + /** + * The document that the editor's container element belongs to. + * Use this instead of the global `document` to support cross-window embedding. + * + * @internal + */ + getContainerDocument(): Document { + return this.getContainer().ownerDocument + } + + /** + * The window that the editor's container element belongs to. + * Use this instead of the global `window` to support cross-window embedding. + * + * @internal + */ + getContainerWindow(): Window & typeof globalThis { + return getOwnerWindow(this.getContainer()) + } + + /** + * Dispose the editor. + * + * @public + */ + dispose() { + // If the editor is disposed while still mounted (for example when its component tree is + // unmounted all at once), emit `unmount` first — while listeners are still attached — so + // that `mount` is always balanced by an `unmount` and `getIsMounted()` reads `false`. + if (this._isMounted.get()) { + this.emit('unmount') + } + // The unmount listener above normally handles this; unregister again in case a listener + // threw or was removed, so a disposed editor never lingers in `tleditors`. + unregisterMountedEditor(this) + + // Stop any in-progress camera animations and following before + // running disposables, so their cleanup listeners fire first + this.stopCameraAnimation() + if (this.getInstanceState().followingUserId) { + this.stopFollowingUser() + } + + this.disposables.forEach((dispose) => dispose()) + this.disposables.clear() + + // Clear any open menus for this editor's context + this.menus.clearOpenMenus() + + this.store.dispose() + this.isDisposed = true + this.emit('dispose') + this.removeAllListeners() + } + + /* ------------------ Themes (shadowing the theme manager) ------------------ */ + + /** + * Get the current color mode (`'light'` or `'dark'`), based on the user's dark mode preference. + * + * @public + */ + getColorMode(): 'light' | 'dark' { + return this._themeManager.getColorMode() + } + + /** + * Set the color mode. Note that this is a convenience method that passes the mode to + * `user.updateUserPreferences`, which is the source of truth for the user's color mode preference. + * + * @public + */ + setColorMode(mode: 'light' | 'dark') { + this.user.updateUserPreferences({ colorScheme: mode }) + return this + } + + /** + * Get the id of the current theme. + * + * @public + */ + getCurrentThemeId(): TLThemeId { + return this._themeManager.getCurrentThemeId() + } + + /** + * Get the current theme definition. + * + * @public + */ + getCurrentTheme(): TLTheme { + return this._themeManager.getCurrentTheme() + } + + /** + * Set the current theme by id. + * + * @public + */ + setCurrentTheme(id: TLThemeId) { + this._themeManager.setCurrentTheme(id) + return this + } + + /** + * Get all registered theme definitions. + * + * @public + */ + getThemes(): TLThemes { + return this._themeManager.getThemes() + } + + /** + * Get a single theme definition by id. + * + * @public + */ + getTheme(id: TLThemeId): TLTheme | undefined { + return this._themeManager.getTheme(id) + } + + /** + * Replace all theme definitions, or update them via a callback that receives a deep copy. + * The `'default'` theme must always be present in the result. + * + * @example + * ```ts + * // Replace all themes + * editor.updateThemes({ default: myDefaultTheme, ocean: myOceanTheme }) + * + * // Update via callback + * editor.updateThemes((themes) => { + * delete themes.ocean + * return themes + * }) + * ``` + * + * @public + */ + updateThemes(themes: TLThemes | ((themes: TLThemes) => TLThemes)) { + this._themeManager.updateThemes(themes) + return this + } + + /** + * Register or update a single theme definition. The theme is keyed by its `id` property. + * + * @example + * ```ts + * // Override a property on the default theme + * editor.updateTheme({ ...editor.getTheme('default')!, fontSize: 24 }) + * + * // Register a new theme + * editor.updateTheme({ id: 'ocean', ...myOceanTheme }) + * ``` + * + * @public + */ + updateTheme(theme: TLTheme) { + this._themeManager.updateTheme(theme) + return this + } + + /* ------------------- Shape Utils ------------------ */ + + /** + * A map of shape utility classes (TLShapeUtils) by shape type. + * + * @public + */ + shapeUtils: { readonly [K in string]?: ShapeUtil } + + /** @internal */ + private _shapeUtilsByAssetType: { readonly [K in string]?: ShapeUtil } = {} + + styleProps: { [key: string]: Map, string> } + + /** + * Get a shape util from a shape itself. + * + * @example + * ```ts + * const util = editor.getShapeUtil(myArrowShape) + * const util = editor.getShapeUtil('arrow') + * const util = editor.getShapeUtil(myArrowShape) + * const util = editor.getShapeUtil(TLArrowShape)('arrow') + * ``` + * + * @param shape - A shape, shape partial, or shape type. + * + * @public + */ + getShapeUtil(type: K): ShapeUtil> + getShapeUtil(shape: S | TLShapePartial | S['type']): ShapeUtil + getShapeUtil(type: T extends ShapeUtil ? R['type'] : string): T + getShapeUtil(arg: string | { type: string }) { + const type = typeof arg === 'string' ? arg : arg.type + const shapeUtil = getOwnProperty(this.shapeUtils, type) + // hot path: avoid building the message (and the assert wrapper) on every successful call + if (!shapeUtil) throw new Error(`No shape util found for type "${type}"`) + return shapeUtil + } + + /** + * Returns true if the editor has a shape util for the given shape / shape type. + * + * @param shape - A shape, shape partial, or shape type. + */ + hasShapeUtil(shape: TLShape | TLShapePartial): boolean + hasShapeUtil(type: TLShape['type']): boolean + hasShapeUtil( + type: T extends ShapeUtil ? R['type'] : string + ): boolean + hasShapeUtil(arg: string | { type: string }): boolean { + const type = typeof arg === 'string' ? arg : arg.type + return hasOwnProperty(this.shapeUtils, type) + } + + /** + * Get the shape util that handles the given asset type. + * Returns the shape util whose {@link ShapeUtil.handledAssetTypes} includes + * the given asset type, or undefined if none matches. + * + * @param assetType - The asset type string. + * @public + */ + getShapeUtilForAssetType(assetType: string): ShapeUtil | undefined { + return getOwnProperty(this._shapeUtilsByAssetType, assetType) + } + + /* ------------------- Binding Utils ------------------ */ + /** + * A map of shape utility classes (TLShapeUtils) by shape type. + * + * @public + */ + bindingUtils: { readonly [K in string]?: BindingUtil } + + /** + * Get a binding util from a binding itself. + * + * @example + * ```ts + * const util = editor.getBindingUtil(myArrowBinding) + * const util = editor.getBindingUtil('arrow') + * const util = editor.getBindingUtil(myArrowBinding) + * const util = editor.getBindingUtil(TLArrowBinding)('arrow') + * ``` + * + * @param binding - A binding, binding partial, or binding type. + * + * @public + */ + getBindingUtil(type: K): BindingUtil> + getBindingUtil(binding: S | { type: S['type'] }): BindingUtil + getBindingUtil( + type: T extends BindingUtil ? R['type'] : string + ): T + getBindingUtil(arg: string | { type: string }) { + const type = typeof arg === 'string' ? arg : arg.type + const bindingUtil = getOwnProperty(this.bindingUtils, type) + assert(bindingUtil, `No binding util found for type "${type}"`) + return bindingUtil + } + + /* ------------------- Asset Utils ------------------ */ + + /** + * A map of asset utility classes by asset type. + * + * @public + */ + assetUtils: { readonly [K in string]?: AssetUtil } = {} + + /** + * Get an asset util from an asset or asset type. + * + * @param arg - An asset, asset type string, or object with type. + * + * @public + */ + getAssetUtil(asset: S | { type: S['type'] }): AssetUtil + getAssetUtil(type: string): AssetUtil + getAssetUtil(arg: string | { type: string }) { + const type = typeof arg === 'string' ? arg : arg.type + const assetUtil = getOwnProperty(this.assetUtils, type) + assert(assetUtil, `No asset util found for type "${type}"`) + return assetUtil + } + + /** + * Returns true if the editor has an asset util for the given asset type. + * + * @public + */ + hasAssetUtil(arg: string | { type: string }): boolean { + const type = typeof arg === 'string' ? arg : arg.type + return hasOwnProperty(this.assetUtils, type) + } + + /** + * Get the asset util that accepts the given MIME type. + * Returns null if no registered asset util accepts the MIME type. + * + * @public + */ + getAssetUtilForMimeType(mimeType: string): AssetUtil | null { + for (const util of Object.values(this.assetUtils)) { + if (util && util.acceptsMimeType(mimeType)) { + return util + } + } + return null + } + + /* --------------------- History -------------------- */ + + /** + * A manager for the editor's history. + * + * @readonly + */ + protected readonly history: HistoryManager + + /** + * Undo to the last mark. + * + * @example + * ```ts + * editor.undo() + * ``` + * + * @public + */ + undo(): this { + this._flushEventsForTick(0) + this.complete() + this.history.undo() + this.performance._notifyUndoRedo('undo', this.history.getNumUndos(), this.history.getNumRedos()) + return this + } + + /** + * Whether the editor can undo. + * + * @public + */ + @computed canUndo(): boolean { + return this.history.getNumUndos() > 0 + } + + getCanUndo() { + return this.canUndo() + } + + /** + * Redo to the next mark. + * + * @example + * ```ts + * editor.redo() + * ``` + * + * @public + */ + redo(): this { + this._flushEventsForTick(0) + this.complete() + this.history.redo() + this.performance._notifyUndoRedo('redo', this.history.getNumUndos(), this.history.getNumRedos()) + return this + } + + /** + * Whether the editor can redo. + * + * @public + */ + @computed canRedo(): boolean { + return this.history.getNumRedos() > 0 + } + + getCanRedo() { + return this.canRedo() + } + + clearHistory() { + this.history.clear() + return this + } + + /** + * Create a new "mark", or stopping point, in the undo redo history. Creating a mark will clear + * any redos. You typically want to do this just before a user interaction begins or is handled. + * + * @example + * ```ts + * editor.markHistoryStoppingPoint() + * editor.flipShapes(editor.getSelectedShapes()) + * ``` + * @example + * ```ts + * const beginRotateMark = editor.markHistoryStoppingPoint() + * // if the use cancels the rotation, you can bail back to this mark + * editor.bailToMark(beginRotateMark) + * ``` + * + * @public + * @param name - The name of the mark, useful for debugging the undo/redo stacks + * @returns a unique id for the mark that can be used with `squashToMark` or `bailToMark`. + */ + markHistoryStoppingPoint(name?: string): string { + const id = `[${name ?? 'stop'}]_${uniqueId()}` + this.history._mark(id) + return id + } + + /** + * @internal this is only used to implement some backwards-compatibility logic. Should be fine to delete after 6 months or whatever. + */ + getMarkIdMatching(idSubstring: string) { + return this.history.getMarkIdMatching(idSubstring) + } + + /** + * Whether the editor is currently replaying history (i.e. an undo or redo is being applied). + * + * @internal + */ + isReplayingHistory(): boolean { + return this.history.isReplaying() + } + + /** + * Coalesces all changes since the given mark into a single change, removing any intermediate marks. + * + * This is useful if you need to 'compress' the recent history to simplify the undo/redo experience of a complex interaction. + * + * @example + * ```ts + * const bumpShapesMark = editor.markHistoryStoppingPoint() + * // ... some changes + * editor.squashToMark(bumpShapesMark) + * ``` + * + * @param markId - The mark id to squash to. + */ + squashToMark(markId: string): this { + this.history.squashToMark(markId) + return this + } + + /** + * Undo to the closest mark, discarding the changes so they cannot be redone. + * + * @example + * ```ts + * editor.bail() + * ``` + * + * @public + */ + bail() { + this.history.bail() + return this + } + + /** + * Undo to the given mark, discarding the changes so they cannot be redone. + * + * @example + * ```ts + * const beginDrag = editor.markHistoryStoppingPoint() + * // ... some changes + * editor.bailToMark(beginDrag) + * ``` + * + * @public + */ + bailToMark(id: string): this { + this.history.bailToMark(id) + return this + } + + private _shouldIgnoreShapeLock = false + + /** + * Run a function in a transaction with optional options for context. + * You can use the options to change the way that history is treated + * or allow changes to locked shapes. + * + * @example + * ```ts + * // updating with + * editor.run(() => { + * editor.updateShape({ ...myShape, x: 100 }) + * }, { history: "ignore" }) + * + * // forcing changes / deletions for locked shapes + * editor.toggleLock([myShape]) + * editor.run(() => { + * editor.updateShape({ ...myShape, x: 100 }) + * editor.deleteShape(myShape) + * }, { ignoreShapeLock: true }, ) + * ``` + * + * @param fn - The callback function to run. + * @param opts - The options for the batch. + * + * + * @public + */ + run(fn: () => void, opts?: TLEditorRunOptions): this { + const previousIgnoreShapeLock = this._shouldIgnoreShapeLock + this._shouldIgnoreShapeLock = opts?.ignoreShapeLock ?? previousIgnoreShapeLock + try { + this.history.batch(fn, opts) + } finally { + this._shouldIgnoreShapeLock = previousIgnoreShapeLock + } + + return this + } + + /* --------------------- Errors --------------------- */ + + /** @internal */ + annotateError( + error: unknown, + { + origin, + willCrashApp, + tags, + extras, + }: { + origin: string + willCrashApp: boolean + tags?: Record + extras?: Record + } + ): this { + const defaultAnnotations = this.createErrorAnnotations(origin, willCrashApp) + annotateError(error, { + tags: { ...defaultAnnotations.tags, ...tags }, + extras: { ...defaultAnnotations.extras, ...extras }, + }) + if (willCrashApp) { + this.store.markAsPossiblyCorrupted() + } + return this + } + + /** @internal */ + createErrorAnnotations(origin: string, willCrashApp: boolean | 'unknown') { + try { + const editingShapeId = this.getEditingShapeId() + return { + tags: { + origin: origin, + willCrashApp, + }, + extras: { + activeStateNode: this.root.getPath(), + selectedShapes: this.getSelectedShapes().map((s) => { + const { props, ...rest } = s + const { text: _text, richText: _richText, ...restProps } = props as any + return { + ...rest, + props: restProps, + } + }), + selectionCount: this.getSelectedShapes().length, + editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined, + inputs: this.inputs.toJson(), + pageState: this.getCurrentPageState(), + instanceState: this.getInstanceState(), + collaboratorCount: this.getCollaboratorsOnCurrentPage().length, + }, + } + } catch { + return { + tags: { + origin: origin, + willCrashApp, + }, + extras: {}, + } + } + } + + /** @internal */ + private _crashingError: unknown | null = null + + /** + * We can't use an `atom` here because there's a chance that when `crashAndReportError` is called, + * we're in a transaction that's about to be rolled back due to the same error we're currently + * reporting. + * + * Instead, to listen to changes to this value, you need to listen to editor's `crash` event. + * + * @internal + */ + getCrashingError() { + return this._crashingError + } + + /** @internal */ + crash(error: unknown): this { + this._crashingError = error + this.store.markAsPossiblyCorrupted() + this.emit('crash', { error }) + return this + } + + /* ------------------- Statechart ------------------- */ + + /** + * The editor's current path of active states. + * + * @example + * ```ts + * editor.getPath() // "select.idle" + * ``` + * + * @public + */ + @computed getPath() { + return this.root.getPath().split('root.')[1] + } + + /** + * Get whether a certain tool (or other state node) is currently active. + * + * @example + * ```ts + * editor.isIn('select') + * editor.isIn('select.brushing') + * ``` + * + * @param path - The path of active states, separated by periods. + * + * @public + */ + isIn(path: string): boolean { + const ids = path.split('.').reverse() + let state = this.root as StateNode + while (ids.length > 0) { + const id = ids.pop() + if (!id) return true + const current = state.getCurrent() + if (current?.id === id) { + if (ids.length === 0) return true + state = current + continue + } else return false + } + return false + } + + /** + * Get whether the state node is in any of the given active paths. + * + * @example + * ```ts + * state.isInAny('select', 'erase') + * state.isInAny('select.brushing', 'erase.idle') + * ``` + * + * @public + */ + isInAny(...paths: string[]): boolean { + return paths.some((path) => this.isIn(path)) + } + + /** + * Set the selected tool. + * + * @example + * ```ts + * editor.setCurrentTool('hand') + * editor.setCurrentTool('hand', { date: Date.now() }) + * ``` + * + * @param id - The id of the tool to select. + * @param info - Arbitrary data to pass along into the transition. + * + * @public + */ + setCurrentTool(id: string, info = {}): this { + this.root.transition(id, info) + return this + } + + /** + * The current selected tool. + * + * @public + */ + @computed getCurrentTool(): StateNode { + return this.root.getCurrent()! + } + + /** + * The id of the current selected tool. + * + * @public + */ + @computed getCurrentToolId(): string { + const currentTool = this.getCurrentTool() + if (!currentTool) return '' + return currentTool.getCurrentToolIdMask() ?? currentTool.id + } + + /** + * Get a descendant by its path. + * + * @example + * ```ts + * editor.getStateDescendant('select') + * editor.getStateDescendant('select.brushing') + * ``` + * + * @param path - The descendant's path of state ids, separated by periods. + * + * @public + */ + getStateDescendant(path: string): T | undefined { + const ids = path.split('.').reverse() + let state = this.root as StateNode + while (ids.length > 0) { + const id = ids.pop() + if (!id) return state as T + const childState = state.children?.[id] + if (!childState) return undefined + state = childState + } + return state as T + } + + /* ---------------- Document Settings --------------- */ + + /** + * The global document settings that apply to all users. + * + * @public + **/ + @computed getDocumentSettings() { + return this.store.get(TLDOCUMENT_ID)! + } + + /** + * Update the global document settings that apply to all users. + * + * @public + **/ + updateDocumentSettings(settings: Partial): this { + if (this.getIsReadonly()) return this + this.run( + () => { + this.store.put([{ ...this.getDocumentSettings(), ...settings }]) + }, + { history: 'ignore' } + ) + return this + } + + /* ----------------- Instance State ----------------- */ + + /** + * The current instance's state. + * + * @public + */ + @computed getInstanceState(): TLInstance { + return this.store.get(TLINSTANCE_ID)! + } + + /** + * Update the instance's state. + * + * @param partial - A partial object to update the instance state with. + * @param historyOptions - History batch options. + * + * @public + */ + updateInstanceState( + partial: Partial>, + historyOptions?: TLHistoryBatchOptions + ): this { + this._updateInstanceState(partial, { history: 'ignore', ...historyOptions }) + + if (partial.isChangingStyle !== undefined) { + this.timers.clearTimeout(this._isChangingStyleTimeout) + if (partial.isChangingStyle === true) { + // If we've set to true, set a new reset timeout to change the value back to false after 1 seconds + this._isChangingStyleTimeout = this.timers.setTimeout(() => { + this._updateInstanceState({ isChangingStyle: false }, { history: 'ignore' }) + }, 1000) + } + } + + return this + } + + /** @internal */ + _updateInstanceState( + partial: Partial>, + opts?: TLHistoryBatchOptions + ) { + this.run(() => { + this.store.put([ + { + ...this.getInstanceState(), + ...partial, + }, + ]) + }, opts) + } + + /** @internal */ + private _isChangingStyleTimeout = -1 as any + + // Menus + + menus = tlmenus.forContext(this.contextId) + + /* --------------------- Cursor --------------------- */ + + /** + * Set the cursor. + * + * No-op when the partial wouldn't change the current cursor — `setCursor` + * is called from pointer-move hot paths (see `updateHoveredOverlayId`, + * various tool states) and skipping redundant writes avoids needlessly + * dirtying instance state. + * + * @param cursor - The cursor to set. + * @public + */ + setCursor(cursor: Partial) { + const current = this.getInstanceState().cursor + if ( + (cursor.type === undefined || cursor.type === current.type) && + (cursor.rotation === undefined || cursor.rotation === current.rotation) + ) { + return this + } + this.updateInstanceState({ cursor: { ...current, ...cursor } }) + return this + } + + /* ------------------- Page State ------------------- */ + + /** + * Page states. + * + * @public + */ + @computed getPageStates(): TLInstancePageState[] { + return this._getPageStatesQuery().get() + } + + /** @internal */ + @computed private _getPageStatesQuery() { + return this.store.query.records('instance_page_state') + } + + /** + * The current page state. + * + * @public + */ + @computed getCurrentPageState(): TLInstancePageState { + return this.store.get(this._getCurrentPageStateId())! + } + + /** @internal */ + @computed private _getCurrentPageStateId() { + return InstancePageStateRecordType.createId(this.getCurrentPageId()) + } + + /** + * Update this instance's page state. + * + * @example + * ```ts + * editor.updateCurrentPageState({ id: 'page1', editingShapeId: 'shape:123' }) + * ``` + * + * @param partial - The partial of the page state object containing the changes. + * + * @public + */ + updateCurrentPageState( + partial: Partial< + Omit + > + ): this { + this._updateCurrentPageState(partial) + return this + } + _updateCurrentPageState(partial: Partial>) { + this.store.update(partial.id ?? this.getCurrentPageState().id, (state) => ({ + ...state, + ...partial, + })) + } + + /** + * The current selected ids. + * + * @public + */ + @computed getSelectedShapeIds() { + return this.getCurrentPageState().selectedShapeIds + } + + /** + * An array containing all of the currently selected shapes. + * + * @public + * @readonly + */ + @computed getSelectedShapes(): TLShape[] { + return compact(this.getSelectedShapeIds().map((id) => this.store.get(id))) + } + + /** + * Select one or more shapes. + * + * @example + * ```ts + * editor.setSelectedShapes(['id1']) + * editor.setSelectedShapes(['id1', 'id2']) + * ``` + * + * @param shapes - The shape (or shape ids) to select. + * + * @public + */ + setSelectedShapes(shapes: TLShapeId[] | TLShape[]): this { + return this.run( + () => { + const ids = shapes.map((shape) => (typeof shape === 'string' ? shape : shape.id)) + const { selectedShapeIds: prevSelectedShapeIds } = this.getCurrentPageState() + const prevSet = new Set(prevSelectedShapeIds) + + if (ids.length === prevSet.size && ids.every((id) => prevSet.has(id))) return null + + this.store.put([{ ...this.getCurrentPageState(), selectedShapeIds: ids }]) + }, + { history: 'record-preserveRedoStack' } + ) + } + + /** + * Determine whether or not any of a shape's ancestors are selected. + * + * @param shape - The shape (or shape id) of the shape to check. + * + * @public + */ + isAncestorSelected(shape: TLShape | TLShapeId): boolean { + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + const _shape = this.getShape(id) + if (!_shape) return false + const selectedShapeIds = this.getSelectedShapeIds() + return !!this.findShapeAncestor(_shape, (parent) => selectedShapeIds.includes(parent.id)) + } + + /** + * Select one or more shapes. + * + * @example + * ```ts + * editor.select('id1') + * editor.select('id1', 'id2') + * ``` + * + * @param shapes - The shape (or the shape ids) to select. + * + * @public + */ + select(...shapes: TLShapeId[] | TLShape[]): this { + const ids = toShapeIds(shapes) + this.setSelectedShapes(ids) + return this + } + + /** + * Remove a shape from the existing set of selected shapes. + * + * @example + * ```ts + * editor.deselect(shape.id) + * ``` + * + * @public + */ + deselect(...shapes: TLShapeId[] | TLShape[]): this { + const ids = toShapeIds(shapes) + const selectedShapeIds = this.getSelectedShapeIds() + if (selectedShapeIds.length > 0 && ids.length > 0) { + this.setSelectedShapes(selectedShapeIds.filter((id) => !ids.includes(id))) + } + return this + } + + /** + * Select all shapes. If the user has selected shapes that share a parent, + * select all shapes within that parent. If the user has not selected any shapes, + * or if the shapes shapes are only on select all shapes on the current page. + * + * @example + * ```ts + * editor.selectAll() + * ``` + * + * @public + */ + selectAll(): this { + let parentToSelectWithinId: TLParentId | null = null + + const selectedShapeIds = this.getSelectedShapeIds() + + // If we have selected shapes, try to find a parent to select within + if (selectedShapeIds.length > 0) { + for (const id of selectedShapeIds) { + const shape = this.getShape(id) + if (!shape) continue + if (parentToSelectWithinId === null) { + // If we haven't found a parent yet, set this parent as the parent to select within + parentToSelectWithinId = shape.parentId + } else if (parentToSelectWithinId !== shape.parentId) { + // If we've found two different parents, we can't select all, do nothing + return this + } + } + } + + // If we haven't found a parent from our selected shapes, select the current page + if (!parentToSelectWithinId) { + parentToSelectWithinId = this.getCurrentPageId() + } + + // Select all the unlocked shapes within the parent. Only the shape's own lock matters here: + // selecting inside a locked frame or group is allowed, mutating is not. + const ids = this.getSortedChildIdsForParent(parentToSelectWithinId) + if (ids.length <= 0) return this + this.setSelectedShapes(ids.filter((id) => !this.getShape(id)?.isLocked)) + return this + } + + /** + * Select the next shape in the reading order or in cardinal order. + * + * @example + * ```ts + * editor.selectAdjacentShape('next') + * ``` + * + * @public + */ + selectAdjacentShape(direction: TLAdjacentDirection) { + const selectedShapeIds = this.getSelectedShapeIds() + const firstParentId = selectedShapeIds[0] ? this.getShape(selectedShapeIds[0])?.parentId : null + const isSelectedWithinContainer = + firstParentId && + selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) && + !isPageId(firstParentId) + // Locked shapes (and children of locked containers) can't be selected by clicking or + // select all, so traversal skips them too + const filteredShapes = this.getCurrentPageShapes().filter( + (shape) => + !this.isShapeOrAncestorLocked(shape) && + (isSelectedWithinContainer ? shape.parentId === firstParentId : isPageId(shape.parentId)) + ) + const readingOrderShapes = this._getShapesInReadingOrder(filteredShapes) + const currentShapeId: TLShapeId | undefined = + selectedShapeIds.length === 1 + ? selectedShapeIds[0] + : readingOrderShapes.find((shape) => selectedShapeIds.includes(shape.id))?.id + + let adjacentShapeId: TLShapeId + if (direction === 'next' || direction === 'prev') { + const shapeIds = readingOrderShapes.map((shape) => shape.id) + // Every candidate can be filtered out (e.g. a locked shape is selected and nothing + // else is unlocked); indexing an empty list would hand getShape undefined + if (shapeIds.length === 0) return + + const currentIndex = currentShapeId ? shapeIds.indexOf(currentShapeId) : -1 + // With no current index, stepping from -1 makes 'prev' land one shape + // before the last; seed it from 0 so it wraps to the last shape. See #10559. + const startIndex = currentIndex === -1 && direction === 'prev' ? 0 : currentIndex + const adjacentIndex = + (startIndex + (direction === 'next' ? 1 : -1) + shapeIds.length) % shapeIds.length + adjacentShapeId = shapeIds[adjacentIndex] + } else { + if (!currentShapeId) return + adjacentShapeId = this.getNearestAdjacentShape(filteredShapes, currentShapeId, direction) + } + + const shape = this.getShape(adjacentShapeId) + if (!shape) return + + this._selectShapesAndZoom([shape.id]) + } + + /** + * Generates a reading order for shapes based on rows grouping. + * Tries to keep a natural reading order (left-to-right, top-to-bottom). + * + * @public + */ + @computed getCurrentPageShapesInReadingOrder(): TLShape[] { + const shapes = this.getCurrentPageShapes().filter((shape) => isPageId(shape.parentId)) + return this._getShapesInReadingOrder(shapes) + } + + private _getShapesInReadingOrder(shapes: TLShape[]): TLShape[] { + const SHALLOW_ANGLE = 20 + const ROW_THRESHOLD = 100 + + const tabbableShapes = shapes.filter((shape) => this.getShapeUtil(shape).canTabTo(shape)) + + if (tabbableShapes.length <= 1) return tabbableShapes + + const shapesWithCenters = tabbableShapes.map((shape) => ({ + shape, + center: this.getShapePageBounds(shape)!.center, + })) + shapesWithCenters.sort((a, b) => a.center.y - b.center.y) + + const rows: Array = [] + + // First, group shapes into rows based on y-coordinates. + for (const shapeWithCenter of shapesWithCenters) { + let rowIndex = -1 + for (let i = rows.length - 1; i >= 0; i--) { + const row = rows[i] + const lastShapeInRow = row[row.length - 1] + + // If the shape is close enough vertically to the last shape in this row. + if (Math.abs(shapeWithCenter.center.y - lastShapeInRow.center.y) < ROW_THRESHOLD) { + rowIndex = i + break + } + } + + // If no suitable row found, create a new row. + if (rowIndex === -1) { + rows.push([shapeWithCenter]) + } else { + rows[rowIndex].push(shapeWithCenter) + } + } + + // Then, sort each row by x-coordinate (left-to-right). + for (const row of rows) { + row.sort((a, b) => a.center.x - b.center.x) + } + + // Finally, apply angle/distance weight adjustments within rows for closely positioned shapes. + for (const row of rows) { + if (row.length <= 2) continue + + for (let i = 0; i < row.length - 2; i++) { + const currentShape = row[i] + const nextShape = row[i + 1] + const nextNextShape = row[i + 2] + + // Only consider adjustment if the next two shapes are relatively close to each other. + const dist1 = Vec.Dist2(currentShape.center, nextShape.center) + const dist2 = Vec.Dist2(currentShape.center, nextNextShape.center) + + // Check if the 2nd shape is actually closer to the current shape. + if (dist2 < dist1 * 0.9) { + // Check if it's a shallow enough angle. + const angle = Math.abs( + Vec.Angle(currentShape.center, nextNextShape.center) * (180 / Math.PI) + ) + if (angle <= SHALLOW_ANGLE) { + // Swap swap. + ;[row[i + 1], row[i + 2]] = [row[i + 2], row[i + 1]] + } + } + } + } + + return rows.flat().map((item) => item.shape) + } + + /** + * Find the nearest adjacent shape in a specific direction. + * + * @public + */ + getNearestAdjacentShape( + shapes: TLShape[], + currentShapeId: TLShapeId, + direction: 'left' | 'right' | 'up' | 'down' + ): TLShapeId { + const directionToAngle = { right: 0, left: 180, down: 90, up: 270 } + const currentShape = this.getShape(currentShapeId) + if (!currentShape) return currentShapeId + + const tabbableShapes = shapes.filter( + (shape) => this.getShapeUtil(shape).canTabTo(shape) && shape.id !== currentShapeId + ) + if (!tabbableShapes.length) return currentShapeId + + const currentCenter = this.getShapePageBounds(currentShape)!.center + const shapesWithCenters = tabbableShapes.map((shape) => ({ + shape, + center: this.getShapePageBounds(shape)!.center, + })) + + // Filter shapes that are in the same direction. + const shapesInDirection = shapesWithCenters.filter(({ center }) => { + const isRight = center.x > currentCenter.x + const isDown = center.y > currentCenter.y + const xDist = center.x - currentCenter.x + const yDist = center.y - currentCenter.y + const isInXDirection = Math.abs(yDist) < Math.abs(xDist) * 2 + const isInYDirection = Math.abs(xDist) < Math.abs(yDist) * 2 + if (direction === 'left' || direction === 'right') { + return isInXDirection && (direction === 'right' ? isRight : !isRight) + } + if (direction === 'up' || direction === 'down') { + return isInYDirection && (direction === 'down' ? isDown : !isDown) + } + }) + + if (shapesInDirection.length === 0) return currentShapeId + + // Ok, now score that subset of shapes. + const lowestScoringShape = minBy(shapesInDirection, ({ center }) => { + // Linear, not squared: the off-axis and diagonal penalties below are page units too, and + // squared distance swamps them, so a nearer diagonal shape beats an aligned one. + const distance = Vec.Dist(currentCenter, center) + + // Distance along the primary axis. + const dirProp = ['left', 'right'].includes(direction) ? 'x' : 'y' + const directionalDistance = Math.abs(center[dirProp] - currentCenter[dirProp]) + + // Distance off the perpendicular to the primary axis. + const offProp = ['left', 'right'].includes(direction) ? 'y' : 'x' + const offAxisDeviation = Math.abs(center[offProp] - currentCenter[offProp]) + + // atan2 gives -180..180, so 'up' is -90; normalize to 0..360 to match 270, and wrap the + // deviation so that 350 is 10 away from 'right' (0), not 350. + const angle = (Vec.Angle(currentCenter, center) * (180 / Math.PI) + 360) % 360 + const rawAngleDeviation = Math.abs(angle - directionToAngle[direction]) + const angleDeviation = Math.min(rawAngleDeviation, 360 - rawAngleDeviation) + + // Calculate final score (lower is better). + // Weight factors to prioritize: + // 1. Shapes directly in line with the current shape + // 2. Shapes closer to the current shape + // 3. Shapes with less angular deviation from the primary direction + return ( + distance * 1.0 + // Base distance + offAxisDeviation * 2.0 + // Heavy penalty for off-axis deviation + (distance - directionalDistance) * 1.5 + // Penalty for diagonal distance + angleDeviation * 0.5 + ) // Slight penalty for angular deviation + }) + + return lowestScoringShape!.shape.id + } + + selectParentShape() { + const selectedShape = this.getOnlySelectedShape() + if (!selectedShape) return + const parentShape = this.getShape(selectedShape.parentId) + if (!parentShape) return + this._selectShapesAndZoom([parentShape.id]) + } + + selectFirstChildShape() { + const selectedShapes = this.getSelectedShapes() + if (!selectedShapes.length) return + const selectedShape = selectedShapes[0] + const children = compact( + this.getSortedChildIdsForParent(selectedShape.id).map((id) => this.getShape(id)) + ) + const sortedChildren = this._getShapesInReadingOrder(children) + if (sortedChildren.length === 0) return + this._selectShapesAndZoom([sortedChildren[0].id]) + } + + private _selectShapesAndZoom(ids: TLShapeId[]) { + this.setSelectedShapes(ids) + this.zoomToSelectionIfOffscreen(256, { + animation: { + duration: this.options.animationMediumMs, + }, + inset: 0, + }) + } + + /** + * Clear the selection. + * + * @example + * ```ts + * editor.selectNone() + * ``` + * + * @public + */ + selectNone(): this { + if (this.getSelectedShapeIds().length > 0) { + this.setSelectedShapes([]) + } + + return this + } + + /** + * The id of the editor's only selected shape. + * + * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id. + * + * @public + * @readonly + */ + @computed getOnlySelectedShapeId(): TLShapeId | null { + return this.getOnlySelectedShape()?.id ?? null + } + + /** + * The editor's only selected shape. + * + * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape. + * + * @public + * @readonly + */ + @computed getOnlySelectedShape(): TLShape | null { + const selectedShapes = this.getSelectedShapes() + return selectedShapes.length === 1 ? selectedShapes[0] : null + } + + /** + * Get the page bounds of all the provided shapes. + * + * @public + */ + getShapesPageBounds(shapeIds: TLShapeId[]): Box | null { + const bounds = compact(shapeIds.map((id) => this.getShapePageBounds(id))) + if (bounds.length === 0) return null + return Box.Common(bounds) + } + + /** + * The current page bounds of all the selected shapes. If the + * selection is rotated, then these bounds are the axis-aligned + * box that the rotated bounds would fit inside of. + * + * @readonly + * + * @public + */ + @computed getSelectionPageBounds(): Box | null { + return this.getShapesPageBounds(this.getSelectedShapeIds()) + } + + /** + * The bounds of the selection bounding box in the current page space. + * + * @readonly + * @public + */ + getSelectionScreenBounds(): Box | undefined { + const bounds = this.getSelectionPageBounds() + if (!bounds) return undefined + const { x, y } = this.pageToScreen(bounds.point) + const zoom = this.getZoomLevel() + return new Box(x, y, bounds.width * zoom, bounds.height * zoom) + } + + /** + * @internal + */ + getShapesSharedRotation(shapeIds: TLShapeId[]) { + let rotation = 0 + for (let i = 0, n = shapeIds.length; i < n; i++) { + const pageRotation = this.getShapePageTransform(shapeIds[i]).rotation() + if (i === 0) { + rotation = pageRotation + } else if (pageRotation !== rotation) { + // There are at least 2 different rotations, so the common rotation is zero + return 0 + } + } + + return rotation + } + + /** + * The rotation of the selection bounding box in the current page space. + * + * @readonly + * @public + */ + @computed getSelectionRotation(): number { + return this.getShapesSharedRotation(this.getSelectedShapeIds()) + } + + /** + * @internal + */ + getShapesRotatedPageBounds(shapeIds: TLShapeId[]): Box | undefined { + if (shapeIds.length === 0) { + return undefined + } + + const selectionRotation = this.getShapesSharedRotation(shapeIds) + if (selectionRotation === 0) { + return this.getShapesPageBounds(shapeIds) ?? undefined + } + + if (shapeIds.length === 1) { + const bounds = this.getShapeGeometry(shapeIds[0]).bounds.clone() + const pageTransform = this.getShapePageTransform(shapeIds[0]) + bounds.point = pageTransform.applyToPoint(bounds.point) + return bounds + } + + // need to 'un-rotate' all the outlines of the existing nodes so we can fit them inside a box + const boxFromRotatedVertices = Box.FromPoints( + shapeIds + .flatMap((id) => + this.getShapePageTransform(id).applyToPoints(this.getShapeGeometry(id).bounds.corners) + ) + .map((p) => p.rot(-selectionRotation)) + ) + // now position box so that it's top-left corner is in the right place + boxFromRotatedVertices.point = boxFromRotatedVertices.point.rot(selectionRotation) + return boxFromRotatedVertices + } + + /** + * The bounds of the selection bounding box in the current page space. + * + * @readonly + * @public + */ + @computed getSelectionRotatedPageBounds(): Box | undefined { + return this.getShapesRotatedPageBounds(this.getSelectedShapeIds()) + } + + /** + * The bounds of the selection bounding box in the current page space. + * + * @readonly + * @public + */ + @computed getSelectionRotatedScreenBounds(): Box | undefined { + const bounds = this.getSelectionRotatedPageBounds() + if (!bounds) return undefined + // Don't use pageToScreen here: it reads the screen bounds without capturing them, so this + // computed would never invalidate when the container moves + const screenBounds = this.getViewportScreenBounds() + const { x: cx, y: cy, z: zoom } = this.getCamera() + return new Box( + (bounds.x + cx) * zoom + screenBounds.x, + (bounds.y + cy) * zoom + screenBounds.y, + bounds.width * zoom, + bounds.height * zoom + ) + } + + // Focus Group + + /** + * The current focused group id. + * + * @public + */ + @computed getFocusedGroupId(): TLShapeId | TLPageId { + return this.getCurrentPageState().focusedGroupId ?? this.getCurrentPageId() + } + + /** + * The current focused group. + * + * @public + */ + @computed getFocusedGroup(): TLShape | undefined { + const focusedGroupId = this.getFocusedGroupId() + return focusedGroupId ? this.getShape(focusedGroupId) : undefined + } + + /** + * Set the current focused group shape. + * + * @param shape - The group shape id (or group shape's id) to set as the focused group shape. + * + * @public + */ + setFocusedGroup(shape: TLShapeId | TLGroupShape | null): this { + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + + if (id !== null) { + const shape = this.getShape(id) + if (!shape) { + throw Error(`Editor.setFocusedGroup: Shape with id ${id} does not exist`) + } + + if (!this.isShapeOfType(shape, 'group')) { + throw Error( + `Editor.setFocusedGroup: Cannot set focused group to shape of type ${shape.type}` + ) + } + } + + if (id === this.getFocusedGroupId()) return this + + return this.run( + () => { + this.store.update(this.getCurrentPageState().id, (s) => ({ ...s, focusedGroupId: id })) + }, + { history: 'record-preserveRedoStack' } + ) + } + + /** + * Exit the current focused group, moving up to the next parent group if there is one. + * + * @public + */ + popFocusedGroupId(): this { + const focusedGroup = this.getFocusedGroup() + + if (focusedGroup) { + // If we have a focused layer, look for an ancestor of the focused shape that is a group + const match = this.findShapeAncestor(focusedGroup, (shape) => + this.isShapeOfType(shape, 'group') + ) + // If we have an ancestor that can become a focused layer, set it as the focused layer + this.setFocusedGroup(match?.id ?? null) + this.select(focusedGroup.id) + } else { + // If there's no parent focused group, then clear the focus layer and clear selection + this.setFocusedGroup(null) + this.selectNone() + } + + return this + } + + /** + * The current editing shape's id. + * + * @public + */ + @computed getEditingShapeId(): TLShapeId | null { + return this.getCurrentPageState().editingShapeId + } + + /** + * The current editing shape. + * + * @public + */ + @computed getEditingShape(): TLShape | undefined { + const editingShapeId = this.getEditingShapeId() + return editingShapeId ? this.getShape(editingShapeId) : undefined + } + + /** + * Whether the shape can be edited. + * + * @param shape - The shape (or shape id) to check if it can be edited. + * @param info - The info about the edit start. + * + * @public + * @returns true if the shape can be edited, false otherwise. + */ + canEditShape(shape: T | null, info?: TLEditStartInfo): shape is T { + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + if (!id) return false // no shape + if (id === this.getEditingShapeId()) return false // already editing this shape + const _shape = this.getShape(id) + if (!_shape) return false // no shape + const util = this.getShapeUtil(_shape) + const _info: TLEditStartInfo = info ?? { type: 'unknown' } + if (!util.canEdit(_shape, _info)) return false // shape is not editable + if (this.getIsReadonly() && !util.canEditInReadonly(_shape)) return false // readonly and no exception + if (this.isShapeOrAncestorLocked(_shape) && !util.canEditWhileLocked(_shape)) return false // locked and no exception. Note here: we're not distinguishing between a locked shape and a shape that is the descendant of a locked shape. + return true // shape is editable + } + + /** + * Set the current editing shape. + * + * @example + * ```ts + * editor.setEditingShape(myShape) + * editor.setEditingShape(myShape.id) + * ``` + * + * @param shape - The shape (or shape id) to set as editing. + * + * @public + */ + setEditingShape(shape: TLShapeId | TLShape | null): this { + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + + // id was provided but the next editing shape was not editable or didn't exist, so do nothing + if (id && !this.canEditShape(id)) return this + + this.run(() => { + // Clean up the previous editing shape. This runs outside the history-ignored batch below, + // otherwise document changes made by onEditEnd (e.g. deleting an empty text shape) are + // never recorded and leave a phantom undo entry whose redo resurrects the shape. + const prevEditingShapeId = this.getEditingShapeId() + if (prevEditingShapeId) { + const prevEditingShape = this.getShape(prevEditingShapeId) + if (prevEditingShape) { + this.getShapeUtil(prevEditingShape).onEditEnd?.(prevEditingShape) + } + } + + this.run( + () => { + // Clean up the editing shape state and rich text editor + this._updateCurrentPageState({ editingShapeId: null }) + this._currentRichTextEditor.set(null) + + if (!id) return + + this.select(id) + this._updateCurrentPageState({ editingShapeId: id }) + + const nextEditingShape = this.getShape(id)! // shape should be there because canEditShape checked it. Possible small chance that onEditEnd deleted it? + this.getShapeUtil(nextEditingShape).onEditStart?.(nextEditingShape) + }, + { history: 'ignore' } + ) + }) + + return this + } + + // Rich text editor + + private _currentRichTextEditor = atom('rich text editor', null as TiptapEditor | null) + + /** + * The current editing shape's text editor. + * + * @public + */ + @computed getRichTextEditor(): TiptapEditor | null { + return this._currentRichTextEditor.get() + } + + /** + * Set the current editing shape's rich text editor. + * + * @example + * ```ts + * editor.setRichTextEditor(richTextEditorView) + * ``` + * + * @param textEditor - The text editor to set as the current editing shape's text editor. + * + * @public + */ + setRichTextEditor(textEditor: TiptapEditor | null) { + this._currentRichTextEditor.set(textEditor) + return this + } + + // Hovered + + /** + * The current hovered shape id. + * + * @readonly + * @public + */ + @computed getHoveredShapeId(): TLShapeId | null { + return this.getCurrentPageState().hoveredShapeId + } + + /** + * The current hovered shape. + * + * @public + */ + @computed getHoveredShape(): TLShape | undefined { + const hoveredShapeId = this.getHoveredShapeId() + return hoveredShapeId ? this.getShape(hoveredShapeId) : undefined + } + /** + * Set the editor's current hovered shape. + * + * @example + * ```ts + * editor.setHoveredShape(myShape) + * editor.setHoveredShape(myShape.id) + * ``` + * + * @param shape - The shape (or shape id) to set as hovered. + * + * @public + */ + setHoveredShape(shape: TLShapeId | TLShape | null): this { + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + if (id === this.getHoveredShapeId()) return this + this.run( + () => { + this.updateCurrentPageState({ hoveredShapeId: id }) + }, + { history: 'ignore' } + ) + return this + } + + // Hinting + + /** + * The editor's current hinting shape ids. + * + * @public + */ + @computed getHintingShapeIds() { + return this.getCurrentPageState().hintingShapeIds + } + + /** + * The editor's current hinting shapes. + * + * @public + */ + @computed getHintingShape() { + const hintingShapeIds = this.getHintingShapeIds() + return compact(hintingShapeIds.map((id) => this.getShape(id))) + } + + /** + * Set the editor's current hinting shapes. + * + * @example + * ```ts + * editor.setHintingShapes([myShape]) + * editor.setHintingShapes([myShape.id]) + * ``` + * + * @param shapes - The shapes (or shape ids) to set as hinting. + * + * @public + */ + setHintingShapes(shapes: TLShapeId[] | TLShape[]): this { + const ids = toShapeIds(shapes) + // always ephemeral + this.run( + () => { + this._updateCurrentPageState({ hintingShapeIds: dedupe(ids) }) + }, + { history: 'ignore' } + ) + return this + } + + // Erasing + + /** + * The editor's current erasing ids. + * + * @public + */ + @computed getErasingShapeIds() { + return this.getCurrentPageState().erasingShapeIds + } + + /** + * The editor's current erasing shapes. + * + * @public + */ + @computed getErasingShapes() { + const erasingShapeIds = this.getErasingShapeIds() + return compact(erasingShapeIds.map((id) => this.getShape(id))) + } + + /** + * Set the editor's current erasing shapes. + * + * @example + * ```ts + * editor.setErasingShapes([myShape]) + * editor.setErasingShapes([myShape.id]) + * ``` + * + * @param shapes - The shapes (or shape ids) to set as hinting. + * + * @public + */ + setErasingShapes(shapes: TLShapeId[] | TLShape[]): this { + // copy before sorting: the caller may pass a store-owned (frozen) array + const ids = toShapeIds(shapes).slice() + ids.sort() // sort the incoming ids + const erasingShapeIds = this.getErasingShapeIds() + this.run( + () => { + // the current ids are also sorted, so a shallow comparison tells us whether they changed + if (!areArraysShallowEqual(ids, erasingShapeIds)) { + this._updateCurrentPageState({ erasingShapeIds: ids }) + } + }, + { history: 'ignore' } + ) + + return this + } + + // Cropping + + /** + * The current cropping shape's id. + * + * @public + */ + getCroppingShapeId() { + return this.getCurrentPageState().croppingShapeId + } + + /** + * Whether the shape can be cropped. + * + * @param shape - The shape (or shape id) to check if it can be cropped. + * + * @public + * @returns true if the shape can be cropped, false otherwise. + */ + canCropShape(shape: T | null): shape is T { + if (!shape) return false + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + if (!id) return false + const _shape = this.getShape(id) + if (!_shape) return false + const util = this.getShapeUtil(_shape) + if (!util.canCrop(_shape)) return false + if (this.getIsReadonly()) return false + if (this.isShapeOrAncestorLocked(_shape)) return false + return true + } + + /** + * Set the current cropping shape. + * + * @example + * ```ts + * editor.setCroppingShape(myShape) + * editor.setCroppingShape(myShape.id) + * ``` + * + * + * @param shape - The shape (or shape id) to set as cropping. + * + * @public + */ + setCroppingShape(shape: TLShapeId | TLShape | null): this { + const id = typeof shape === 'string' ? shape : (shape?.id ?? null) + if (id !== this.getCroppingShapeId()) { + this.run( + () => { + if (!id) { + this.updateCurrentPageState({ croppingShapeId: null }) + } else if (this.canCropShape(id)) { + this.updateCurrentPageState({ croppingShapeId: id }) + } + }, + { history: 'ignore' } + ) + } + return this + } + + private _textOptions: Atom + + /** + * Get the current text options. + * + * @example + * ```ts + * editor.getTextOptions() + * ``` + * + * @public */ + getTextOptions() { + return assertExists(this._textOptions.get(), 'Cannot use text without setting textOptions') + } + + /* --------------------- Camera --------------------- */ + + /** @internal */ + @computed + private _unsafe_getCameraId() { + return CameraRecordType.createId(this.getCurrentPageId()) + } + + /** + * The current camera. + * + * @public + */ + @computed getCamera(): TLCamera { + const baseCamera = this.store.get(this._unsafe_getCameraId())! + if (this._isLockedOnFollowingUser.get()) { + const followingCamera = this.getCameraForFollowing() + if (followingCamera) { + return { ...baseCamera, ...followingCamera } + } + } + return baseCamera + } + + private _getFollowingPresence(targetUserId: TLUserId | null) { + const visited = [this.user.getRecordId()] + const collaborators = this.getCollaborators() + let leaderPresence = null as null | TLInstancePresence + while (targetUserId && !visited.includes(targetUserId)) { + const nextPresence = collaborators.find((c) => c.userId === targetUserId) + // Stop at the last resolvable presence, otherwise a leader whose own leader has left + // (or whose presence hasn't arrived yet) can't be followed at all + if (!nextPresence) break + leaderPresence = nextPresence + targetUserId = nextPresence.followingUserId ?? null + visited.push(nextPresence.userId) + } + return leaderPresence + } + + @computed + private getViewportPageBoundsForFollowing(): null | Box { + const leaderPresence = this._getFollowingPresence(this.getInstanceState().followingUserId) + + if (!leaderPresence?.camera || !leaderPresence?.screenBounds) return null + + // Fit their viewport inside of our screen bounds + // 1. calculate their viewport in page space + const { w: lw, h: lh } = leaderPresence.screenBounds + const { x: lx, y: ly, z: lz } = leaderPresence.camera + const theirViewport = new Box(-lx, -ly, lw / lz, lh / lz) + + // resize our screenBounds to contain their viewport + const ourViewport = this.getViewportScreenBounds().clone() + const ourAspectRatio = ourViewport.width / ourViewport.height + + ourViewport.width = theirViewport.width + ourViewport.height = ourViewport.width / ourAspectRatio + if (ourViewport.height < theirViewport.height) { + ourViewport.height = theirViewport.height + ourViewport.width = ourViewport.height * ourAspectRatio + } + + ourViewport.center = theirViewport.center + return ourViewport + } + + @computed + private getCameraForFollowing(): null | { x: number; y: number; z: number } { + const viewport = this.getViewportPageBoundsForFollowing() + if (!viewport) return null + + return { + x: -viewport.x, + y: -viewport.y, + z: this.getViewportScreenBounds().w / viewport.width, + } + } + + /** + * The current camera zoom level. + * + * @public + */ + @computed getZoomLevel() { + return this.getCamera().z + } + + /** + * Get the scale factor used when creating or resizing shapes in dynamic size mode. + * + * @public + */ + @computed getResizeScaleFactor() { + return this.user.getIsDynamicResizeMode() ? 1 / this.getZoomLevel() : 1 + } + + private _debouncedZoomLevel = atom('debounced zoom level', 1) + + /** + * Get the debounced zoom level. When the camera is moving, this returns the zoom level + * from when the camera started moving rather than the current zoom level. This can be + * used to avoid expensive re-renders during camera movements. + * + * This behavior is controlled by the `useDebouncedZoom` option. When `useDebouncedZoom` + * is `false`, this method always returns the current zoom level. + * + * @public + */ + @computed getDebouncedZoomLevel() { + if (this.options.debouncedZoom) { + if (this.getCameraState() === 'idle') { + return this.getZoomLevel() + } else { + return this._debouncedZoomLevel.get() + } + } + + return this.getZoomLevel() + } + + @computed private _getAboveDebouncedZoomThreshold() { + return this.getCurrentPageShapeIds().size > this.options.debouncedZoomThreshold + } + + /** + * Get the efficient zoom level. This returns the current zoom level if there are less than a certain number of shapes on the page, + * otherwise it returns the debounced zoom level. This can be used to avoid expensive re-renders during camera movements. + * + * @public + * @example + * ```ts + * editor.getEfficientZoomLevel() + * ``` + * + * @public + */ + @computed getEfficientZoomLevel() { + return this._getAboveDebouncedZoomThreshold() + ? this.getDebouncedZoomLevel() + : this.getZoomLevel() + } + + /** + * Get the camera's initial or reset zoom level. + * + * @example + * ```ts + * editor.getInitialZoom() + * ``` + * + * @public */ + getInitialZoom() { + return this._getFitZoom(this.getCameraOptions().constraints?.initialZoom ?? 'default') + } + + /** + * Get the camera's base level for calculating actual zoom levels based on the zoom steps. + * + * @example + * ```ts + * editor.getBaseZoom() + * ``` + * + * @public */ + getBaseZoom() { + return this._getFitZoom(this.getCameraOptions().constraints?.baseZoom ?? 'default') + } + + private _getFitZoom(fit: TLCameraConstraints['initialZoom']): number { + const cameraOptions = this.getCameraOptions() + if (!cameraOptions.constraints || fit === 'default') return 1 + + const { zx, zy } = getCameraFitXFitY(this, cameraOptions) + + switch (fit) { + case 'fit-min': { + return Math.max(zx, zy) + } + case 'fit-max': { + return Math.min(zx, zy) + } + case 'fit-x': { + return zx + } + case 'fit-y': { + return zy + } + case 'fit-min-100': { + return Math.min(1, Math.max(zx, zy)) + } + case 'fit-max-100': { + return Math.min(1, Math.min(zx, zy)) + } + case 'fit-x-100': { + return Math.min(1, zx) + } + case 'fit-y-100': { + return Math.min(1, zy) + } + default: { + throw exhaustiveSwitchError(fit) + } + } + } + + private _cameraOptions = atom('camera options', DEFAULT_CAMERA_OPTIONS) + + /** + * Get the current camera options. + * + * @example + * ```ts + * editor.getCameraOptions() + * ``` + * + * @public */ + getCameraOptions() { + return this._cameraOptions.get() + } + + /** + * Set the camera options. Changing the options won't immediately change the camera itself, so you may want to call `setCamera` after changing the options. + * + * @example + * ```ts + * editor.setCameraOptions(myCameraOptions) + * editor.setCamera(editor.getCamera()) + * ``` + * + * @param opts - The camera options to set. + * + * @public */ + setCameraOptions(opts: Partial) { + const next = structuredClone({ + ...this._cameraOptions.__unsafe__getWithoutCapture(), + ...opts, + }) + // `undefined < 1` is false, so an explicit `zoomSteps: undefined` would otherwise get through + // and make every later camera move throw + if (!next.zoomSteps || next.zoomSteps.length < 1) next.zoomSteps = [1] + this._cameraOptions.set(next) + this.setCamera(this.getCamera()) + return this + } + + /** @internal */ + private getConstrainedCamera( + point: VecLike, + opts?: TLCameraMoveOptions + ): { + x: number + y: number + z: number + } { + const currentCamera = this.getCamera() + + let { x, y, z = currentCamera.z } = point + + // `requested` kept the caller's focal point (e.g. the cursor) fixed at + // zoom `rz`. When `rz` gets clamped, keep that same focal point fixed at + // the clamped zoom `z` rather than snapping to the viewport center. + const preserveFocalPoint = (current: number, requested: number, rz: number, z: number) => { + const cz = currentCamera.z + if (rz === cz) return current + return current + ((requested - current) * (1 / z - 1 / cz)) / (1 / rz - 1 / cz) + } + + // If force is true, then we'll set the camera to the point regardless of + // the camera options, so that we can handle gestures that permit elasticity + // or decay, or animations that occur while the camera is locked. + if (!opts?.force) { + // Apply any adjustments based on the camera options + + const cameraOptions = this.getCameraOptions() + + const zoomMin = cameraOptions.zoomSteps[0] + const zoomMax = last(cameraOptions.zoomSteps)! + + const vsb = this.getViewportScreenBounds() + + // If bounds are provided, then we'll keep those bounds on screen + if (cameraOptions.constraints) { + const { constraints } = cameraOptions + + // Clamp padding to half the viewport size on either dimension + const px = Math.min(constraints.padding.x, vsb.w / 2) + const py = Math.min(constraints.padding.y, vsb.h / 2) + + // Expand the bounds by the padding + const bounds = Box.From(cameraOptions.constraints.bounds) + + // For each axis, the "natural zoom" is the zoom at + // which the expanded bounds (with padding) would fit + // the current viewport screen bounds. Paddings are + // equal to screen pixels at 100% + // The min and max zooms are factors of the smaller natural zoom axis + + const zx = (vsb.w - px * 2) / bounds.w + const zy = (vsb.h - py * 2) / bounds.h + + const baseZoom = this.getBaseZoom() + const maxZ = zoomMax * baseZoom + const minZ = zoomMin * baseZoom + + if (opts?.reset) { + z = this.getInitialZoom() + } + + if (z < minZ || z > maxZ) { + // We're trying to zoom out past the minimum zoom level, or in + // past the maximum zoom level, so clamp the zoom while keeping + // the caller's focal point fixed. Axis constraints below still + // apply on top of this. + const rz = z + z = clamp(z, minZ, maxZ) + x = preserveFocalPoint(currentCamera.x, x, rz, z) + y = preserveFocalPoint(currentCamera.y, y, rz, z) + } + + // Calculate available space + const minX = px / z - bounds.x + const minY = py / z - bounds.y + const freeW = (vsb.w - px * 2) / z - bounds.w + const freeH = (vsb.h - py * 2) / z - bounds.h + const originX = minX + freeW * constraints.origin.x + const originY = minY + freeH * constraints.origin.y + + const behaviorX = + typeof constraints.behavior === 'string' ? constraints.behavior : constraints.behavior.x + const behaviorY = + typeof constraints.behavior === 'string' ? constraints.behavior : constraints.behavior.y + + // x axis + + if (opts?.reset) { + // Reset the camera according to the origin + x = originX + y = originY + } else { + // Apply constraints to the camera + switch (behaviorX) { + case 'fixed': { + // Center according to the origin + x = originX + break + } + case 'contain': { + // When below fit zoom, center the camera + if (z < zx) x = originX + // When above fit zoom, keep the bounds within padding distance of the viewport edge + else x = clamp(x, minX + freeW, minX) + break + } + case 'inside': { + // When below fit zoom, constrain the camera so that the bounds stay completely within the viewport + if (z < zx) x = clamp(x, minX, (vsb.w - px) / z - bounds.w - bounds.x) + // When above fit zoom, keep the bounds within padding distance of the viewport edge + else x = clamp(x, minX + freeW, minX) + break + } + case 'outside': { + // Constrain the camera so that the bounds never leaves the viewport + x = clamp(x, px / z - bounds.w - bounds.x, (vsb.w - px) / z - bounds.x) + break + } + case 'free': { + // noop, use whatever x is provided + break + } + default: { + throw exhaustiveSwitchError(behaviorX) + } + } + + // y axis + + switch (behaviorY) { + case 'fixed': { + y = originY + break + } + case 'contain': { + if (z < zy) y = originY + else y = clamp(y, minY + freeH, minY) + break + } + case 'inside': { + if (z < zy) y = clamp(y, minY, (vsb.h - py) / z - bounds.h - bounds.y) + else y = clamp(y, minY + freeH, minY) + break + } + case 'outside': { + y = clamp(y, py / z - bounds.h - bounds.y, (vsb.h - py) / z - bounds.y) + break + } + case 'free': { + // noop, use whatever x is provided + break + } + default: { + throw exhaustiveSwitchError(behaviorY) + } + } + } + } else { + // constrain the zoom, keeping the caller's focal point fixed + if (z > zoomMax || z < zoomMin) { + const rz = z + z = clamp(z, zoomMin, zoomMax) + x = preserveFocalPoint(currentCamera.x, x, rz, z) + y = preserveFocalPoint(currentCamera.y, y, rz, z) + } + } + } + + return { x, y, z } + } + + /** @internal */ + private _setCamera(point: VecLike, opts?: TLCameraMoveOptions): this { + const currentCamera = this.getCamera() + + const { x, y, z } = this.getConstrainedCamera(point, opts) + + if (currentCamera.x === x && currentCamera.y === y && currentCamera.z === z) { + return this + } + + transact(() => { + const camera = { ...currentCamera, x, y, z } + this.run( + () => { + this.store.put([camera]) // include id and meta here + }, + { history: 'ignore' } + ) + + // Dispatch a new pointer move because the pointer's page will have changed + // (its screen position will compute to a new page position given the new camera position) + const currentScreenPoint = this.inputs.getCurrentScreenPoint() + const currentPagePoint = this.inputs.getCurrentPagePoint() + + // compare the next page point (derived from the current camera) to the current page point + if ( + currentScreenPoint.x / z - x !== currentPagePoint.x || + currentScreenPoint.y / z - y !== currentPagePoint.y + ) { + // If it's changed, dispatch a pointer event + this.updatePointer({ + immediate: opts?.immediate, + pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE, + }) + } + + this._tickCameraState() + }) + + return this + } + + /** + * Set the current camera. + * + * @example + * ```ts + * editor.setCamera({ x: 0, y: 0}) + * editor.setCamera({ x: 0, y: 0, z: 1.5}) + * editor.setCamera({ x: 0, y: 0, z: 1.5}, { animation: { duration: 1000, easing: (t) => t * t } }) + * ``` + * + * @param point - The new camera position. + * @param opts - The camera move options. + * + * @public + */ + setCamera(point: VecLike, opts?: TLCameraMoveOptions): this { + const { isLocked } = this._cameraOptions.__unsafe__getWithoutCapture() + if (isLocked && !opts?.force) return this + + // Resolve the zoom before building the Vec: Vec.Cast would default a missing z to 1, + // and a missing z should keep the current zoom level instead + const _point = new Vec(point.x, point.y, point.z ?? this.getZoomLevel()) + + // Reject non-finite values before anything else, so the call is a no-op rather than a + // partial one. An animated move writes the camera from a 'tick' listener, and a listener + // that throws stops TickManager scheduling the next frame, which kills every frame-driven + // behavior for the rest of the session instead of surfacing the error to the caller. + if (!Number.isFinite(_point.x) || !Number.isFinite(_point.y) || !Number.isFinite(_point.z)) { + throw Error( + `Editor.setCamera: expected finite values, got (${_point.x}, ${_point.y}, ${_point.z}).` + ) + } + + // Stop any camera animations + this.stopCameraAnimation() + + // Stop following any user + if (this.getInstanceState().followingUserId) { + this.stopFollowingUser() + } + + const camera = this.getConstrainedCamera(_point, opts) + + if (opts?.animation) { + const { width, height } = this.getViewportScreenBounds() + this._animateToViewport( + new Box(-camera.x, -camera.y, width / camera.z, height / camera.z), + opts + ) + } else { + this._setCamera(camera, { + ...opts, + // we already did the constraining, so we don't need to do it again + force: true, + }) + } + + return this + } + + /** + * Center the camera on a point (in the current page space). + * + * @example + * ```ts + * editor.centerOnPoint({ x: 100, y: 100 }) + * editor.centerOnPoint({ x: 100, y: 100 }, { animation: { duration: 200 } }) + * ``` + * + * @param point - The point in the current page space to center on. + * @param opts - The camera move options. + * + * @public + */ + centerOnPoint(point: VecLike, opts?: TLCameraMoveOptions): this { + const { isLocked } = this.getCameraOptions() + if (isLocked && !opts?.force) return this + + const { width: pw, height: ph } = this.getViewportPageBounds() + this.setCamera(new Vec(-(point.x - pw / 2), -(point.y - ph / 2), this.getCamera().z), opts) + return this + } + + /** + * Zoom the camera to fit the current page's content in the viewport. + * + * @example + * ```ts + * editor.zoomToFit() + * editor.zoomToFit({ animation: { duration: 200 } }) + * ``` + * + * @param opts - The camera move options. + * + * @public + */ + zoomToFit(opts?: TLCameraMoveOptions): this { + const ids = [...this.getCurrentPageShapeIds()].filter((id) => !this.isShapeHidden(id)) + if (ids.length <= 0) return this + const pageBounds = Box.Common(compact(ids.map((id) => this.getShapePageBounds(id)))) + this.zoomToBounds(pageBounds, opts) + return this + } + + /** + * Set the zoom back to 100%. + * + * @example + * ```ts + * editor.resetZoom() + * editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } }) + * editor.resetZoom(editor.getViewportScreenCenter(), { animation: { duration: 200 } }) + * ``` + * + * @param point - The screen point to zoom out on. Defaults to the viewport screen center. + * @param opts - The camera move options. + * + * @public + */ + resetZoom(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { + const { isLocked, constraints: constraints } = this.getCameraOptions() + if (isLocked && !opts?.force) return this + + const currentCamera = this.getCamera() + const { x: cx, y: cy, z: cz } = currentCamera + const { x, y } = point + + let z = 1 + + if (constraints) { + // For non-infinite fit, we'll set the camera to the natural zoom level... + // unless it's already there, in which case we'll set zoom to 100% + const initialZoom = this.getInitialZoom() + if (cz !== initialZoom) { + z = initialZoom + } + } + + this.setCamera( + new Vec(cx + (x / z - x) - (x / cz - x), cy + (y / z - y) - (y / cz - y), z), + opts + ) + return this + } + + /** + * Zoom the camera in. + * + * @example + * ```ts + * editor.zoomIn() + * editor.zoomIn(editor.getViewportScreenCenter(), { animation: { duration: 200 } }) + * editor.zoomIn(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 200 } }) + * ``` + * + * @param point - The screen point to zoom in on. Defaults to the screen center + * @param opts - The camera move options. + * + * @public + */ + zoomIn(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { + const { isLocked } = this.getCameraOptions() + if (isLocked && !opts?.force) return this + + const { x: cx, y: cy, z: cz } = this.getCamera() + + const { zoomSteps } = this.getCameraOptions() + if (zoomSteps !== null && zoomSteps.length > 1) { + const baseZoom = this.getBaseZoom() + let zoom = last(zoomSteps)! * baseZoom + for (let i = 1; i < zoomSteps.length; i++) { + const z1 = zoomSteps[i - 1] * baseZoom + const z2 = zoomSteps[i] * baseZoom + if (z2 - cz <= (z2 - z1) / 2) continue + zoom = z2 + break + } + this.setCamera( + new Vec( + cx + (point.x / zoom - point.x) - (point.x / cz - point.x), + cy + (point.y / zoom - point.y) - (point.y / cz - point.y), + zoom + ), + opts + ) + } + + return this + } + + /** + * Zoom the camera out. + * + * @example + * ```ts + * editor.zoomOut() + * editor.zoomOut(editor.getViewportScreenCenter(), { animation: { duration: 120 } }) + * editor.zoomOut(editor.inputs.getCurrentScreenPoint(), { animation: { duration: 120 } }) + * ``` + * + * @param point - The point to zoom out on. Defaults to the viewport screen center. + * @param opts - The camera move options. + * + * @public + */ + zoomOut(point = this.getViewportScreenCenter(), opts?: TLCameraMoveOptions): this { + const { isLocked } = this.getCameraOptions() + if (isLocked && !opts?.force) return this + + const { zoomSteps } = this.getCameraOptions() + if (zoomSteps !== null && zoomSteps.length > 1) { + const baseZoom = this.getBaseZoom() + const { x: cx, y: cy, z: cz } = this.getCamera() + // start at the max + let zoom = zoomSteps[0] * baseZoom + for (let i = zoomSteps.length - 1; i > 0; i--) { + const z1 = zoomSteps[i - 1] * baseZoom + const z2 = zoomSteps[i] * baseZoom + if (z2 - cz >= (z2 - z1) / 2) continue + zoom = z1 + break + } + this.setCamera( + new Vec( + cx + (point.x / zoom - point.x) - (point.x / cz - point.x), + cy + (point.y / zoom - point.y) - (point.y / cz - point.y), + zoom + ), + opts + ) + } + + return this + } + + /** + * Zoom the camera to fit the current selection in the viewport. + * + * @example + * ```ts + * editor.zoomToSelection() + * editor.zoomToSelection({ animation: { duration: 200 } }) + * ``` + * + * @param opts - The camera move options. + * + * @public + */ + zoomToSelection(opts?: TLCameraMoveOptions): this { + const { isLocked } = this.getCameraOptions() + if (isLocked && !opts?.force) return this + + const selectionPageBounds = this.getSelectionPageBounds() + if (selectionPageBounds) { + const currentZoom = this.getZoomLevel() + // If already at 100%, zoom to fit the selection in the viewport + // Otherwise, zoom to 100% centered on the selection + if (Math.abs(currentZoom - 1) < 0.01) { + this.zoomToBounds(selectionPageBounds, opts) + } else { + this.zoomToBounds(selectionPageBounds, { + targetZoom: 1, + ...opts, + }) + } + } + return this + } + + /** + * Zoom the camera to the current selection if offscreen. + * + * @public + */ + zoomToSelectionIfOffscreen( + padding = 16, + opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions + ) { + const selectionPageBounds = this.getSelectionPageBounds() + const viewportPageBounds = this.getViewportPageBounds() + if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) { + const eb = selectionPageBounds + .clone() + // Expand the bounds by the padding + .expandBy(padding / this.getZoomLevel()) + // then expand the bounds to include the viewport bounds + .expand(viewportPageBounds) + + // then use the difference between the centers to calculate the offset + const nextBounds = viewportPageBounds.clone().translate({ + x: (eb.center.x - viewportPageBounds.center.x) * 2, + y: (eb.center.y - viewportPageBounds.center.y) * 2, + }) + this.zoomToBounds(nextBounds, opts) + } + } + + /** + * Zoom the camera to fit a bounding box (in the current page space). + * + * @example + * ```ts + * editor.zoomToBounds(myBounds) + * editor.zoomToBounds(myBounds, { animation: { duration: 200 } }) + * editor.zoomToBounds(myBounds, { animation: { duration: 200 }, inset: 0, targetZoom: 1 }) + * ``` + * + * @param bounds - The bounding box. + * @param opts - The camera move options, target zoom, or custom inset amount. + * + * @public + */ + zoomToBounds( + bounds: BoxLike, + opts?: { targetZoom?: number; inset?: number } & TLCameraMoveOptions + ): this { + const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture() + if (cameraOptions.isLocked && !opts?.force) return this + + const viewportScreenBounds = this.getViewportScreenBounds() + + const inset = + opts?.inset ?? Math.min(this.options.zoomToFitPadding, viewportScreenBounds.width * 0.28) + + const baseZoom = this.getBaseZoom() + const zoomMin = cameraOptions.zoomSteps[0] + const zoomMax = last(cameraOptions.zoomSteps)! + + let zoom = clamp( + Math.min( + (viewportScreenBounds.width - inset) / bounds.w, + (viewportScreenBounds.height - inset) / bounds.h + ), + zoomMin * baseZoom, + zoomMax * baseZoom + ) + + if (opts?.targetZoom !== undefined) { + zoom = Math.min(opts.targetZoom, zoom) + } + + this.setCamera( + new Vec( + -bounds.x + (viewportScreenBounds.width - bounds.w * zoom) / 2 / zoom, + -bounds.y + (viewportScreenBounds.height - bounds.h * zoom) / 2 / zoom, + zoom + ), + opts + ) + + return this + } + + /** + * Stop the current camera animation, if any. + * + * @example + * ```ts + * editor.stopCameraAnimation() + * ``` + * + * @public + */ + stopCameraAnimation(): this { + this.emit('stop-camera-animation') + return this + } + + /** @internal */ + private _viewportAnimation = null as null | { + elapsed: number + duration: number + easing(t: number): number + start: Box + end: Box + opts: TLCameraMoveOptions + } + + /** @internal */ + private _animateViewport(ms: number): void { + if (!this._viewportAnimation) return + + this._viewportAnimation.elapsed += ms + + const { elapsed, easing, duration, start, end, opts } = this._viewportAnimation + + if (elapsed > duration) { + this.off('tick', this._animateViewport) + this._viewportAnimation = null + // Forward the caller's options, otherwise a forced move to a position outside the + // constraints animates there and then snaps back on this last frame + this._setCamera( + new Vec(-end.x, -end.y, this.getViewportScreenBounds().width / end.width), + opts + ) + return + } + + const remaining = duration - elapsed + const t = easing(1 - remaining / duration) + + const left = lerp(start.minX, end.minX, t) + const top = lerp(start.minY, end.minY, t) + const right = lerp(start.maxX, end.maxX, t) + + this._setCamera(new Vec(-left, -top, this.getViewportScreenBounds().width / (right - left)), { + force: true, + }) + } + + /** @internal */ + private _animateToViewport( + targetViewportPage: Box, + opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions + ) { + const { animation, ...rest } = opts + if (!animation) return + const { duration = 0, easing = EASINGS.easeInOutCubic } = animation + const animationSpeed = this.user.getAnimationSpeed() + const viewportPageBounds = this.getViewportPageBounds() + + // If we have an existing animation, then stop it + this.stopCameraAnimation() + + // also stop following any user + if (this.getInstanceState().followingUserId) { + this.stopFollowingUser() + } + + if (duration === 0 || animationSpeed === 0) { + // If we have no animation, then skip the animation and just set the camera + return this._setCamera( + new Vec( + -targetViewportPage.x, + -targetViewportPage.y, + this.getViewportScreenBounds().width / targetViewportPage.width + ), + { ...rest } + ) + } + + // Set our viewport animation + this._viewportAnimation = { + elapsed: 0, + duration: duration / animationSpeed, + easing, + start: viewportPageBounds.clone(), + end: targetViewportPage.clone(), + opts: rest, + } + + // If we ever get a "stop-camera-animation" event, we stop + this.once('stop-camera-animation', () => { + this.off('tick', this._animateViewport) + this._viewportAnimation = null + }) + + // On each tick, animate the viewport + this.on('tick', this._animateViewport) + + return this + } + + /** + * Slide the camera in a certain direction. + * + * @example + * ```ts + * editor.slideCamera({ speed: 1, direction: { x: 1, y: 0 }, friction: 0.1 }) + * ``` + * + * @param opts - Options for the slide + * @public + */ + slideCamera( + opts = {} as { + speed: number + direction: VecLike + friction?: number + speedThreshold?: number + force?: boolean + } + ): this { + const { isLocked } = this.getCameraOptions() + if (isLocked && !opts?.force) return this + + const animationSpeed = this.user.getAnimationSpeed() + if (animationSpeed === 0) return this + + this.stopCameraAnimation() + + const { + speed, + friction = this.options.cameraSlideFriction, + direction, + speedThreshold = 0.01, + } = opts + let currentSpeed = Math.min(speed, 1) + + const cancel = () => { + this.off('tick', moveCamera) + this.off('stop-camera-animation', cancel) + } + + this.once('stop-camera-animation', cancel) + + const dirZ = direction.z ?? 0 + + const moveCamera = (elapsed: number) => { + const { x: cx, y: cy, z: cz } = this.getCamera() + + // Pan movement from x/y direction + const dx = (direction.x * (currentSpeed * elapsed)) / cz + const dy = (direction.y * (currentSpeed * elapsed)) / cz + + let newCx = cx + dx + let newCy = cy + dy + let newCz = cz + + // animate zoom if z direction is passed in. Use an exponential factor so that a single + // long frame can't drive the zoom to zero or negative (which would give NaN coordinates) + if (dirZ !== 0) { + newCz = cz * Math.exp(dirZ * currentSpeed * elapsed) + // Adjust x/y to keep the viewport center fixed while zooming + const center = this.getViewportScreenCenter() + newCx += center.x / newCz - center.x / cz + newCy += center.y / newCz - center.y / cz + } + + // Apply friction per unit of elapsed time, not per tick, or a 120 Hz display decays twice as fast + currentSpeed *= (1 - friction) ** (elapsed / FRAME_MS_60HZ) + if (currentSpeed < speedThreshold) { + cancel() + } else { + this._setCamera(new Vec(newCx, newCy, newCz)) + } + } + + this.on('tick', moveCamera) + + return this + } + + /** + * Animate the camera to a user's cursor position. This also briefly show the user's cursor if it's not currently visible. + * + * @example + * ```ts + * editor.zoomToUser(myUserId) + * editor.zoomToUser(myUserId, { animation: { duration: 200 } }) + * ``` + * + * @param userId - The id of the user to animate to. + * @param opts - The camera move options. + * @public + */ + zoomToUser(userId: TLUserId, opts: TLCameraMoveOptions = { animation: { duration: 500 } }): this { + const presence = this.getCollaborators().find((c) => c.userId === userId) + + if (!presence) return this + + const cursor = presence.cursor + if (!cursor) return this + + this.run(() => { + // If we're following someone, stop following them + if (this.getInstanceState().followingUserId !== null) { + this.stopFollowingUser() + } + + // If we're not on the same page, move to the page they're on + const isOnSamePage = presence.currentPageId === this.getCurrentPageId() + if (!isOnSamePage) { + this.markHistoryStoppingPoint('change-page') + this.setCurrentPage(presence.currentPageId) + } + + // Only animate the camera if the user is on the same page as us + if (opts && opts.animation && !isOnSamePage) { + opts.animation = undefined + } + + this.centerOnPoint(cursor, opts) + + // Highlight the user's cursor + const { highlightedUserIds } = this.getInstanceState() + this.updateInstanceState({ highlightedUserIds: [...highlightedUserIds, userId] }) + + // Unhighlight the user's cursor after a few seconds + this.timers.setTimeout(() => { + const highlightedUserIds = [...this.getInstanceState().highlightedUserIds] + const index = highlightedUserIds.indexOf(userId) + if (index < 0) return + highlightedUserIds.splice(index, 1) + this.updateInstanceState({ highlightedUserIds }) + }, this.options.collaboratorIdleTimeoutMs) + }) + + return this + } + + // Viewport + + /** @internal */ + private _willSetInitialBounds = true + + /** + * Update the viewport. The viewport will measure the size and screen position of its container + * element. This should be done whenever the container's position on the screen changes. + * + * @example + * ```ts + * editor.updateViewportScreenBounds(new Box(0, 0, 1280, 1024)) + * editor.updateViewportScreenBounds(new Box(0, 0, 1280, 1024), true) + * ``` + * + * @param screenBounds - The new screen bounds of the viewport. + * @param center - Whether to preserve the viewport page center as the viewport changes. + * + * @public + */ + updateViewportScreenBounds(screenBounds: Box | HTMLElement, center = false): this { + if (!(screenBounds instanceof Box)) { + const rect = screenBounds.getBoundingClientRect() + screenBounds = new Box( + rect.left || rect.x, + rect.top || rect.y, + Math.max(rect.width, 1), + Math.max(rect.height, 1) + ) + } else { + screenBounds.width = Math.max(screenBounds.width, 1) + screenBounds.height = Math.max(screenBounds.height, 1) + } + + const doc = this.getContainerDocument() + // If the container's document has been torn down (e.g. an iframe being + // removed), its body is null and there's nothing meaningful to measure. + if (!doc.body) return this + + const insets = [ + // top + screenBounds.minY !== 0, + // right + !approximately(doc.body.scrollWidth, screenBounds.maxX, 1), + // bottom + !approximately(doc.body.scrollHeight, screenBounds.maxY, 1), + // left + screenBounds.minX !== 0, + ] + + const { _willSetInitialBounds } = this + + this._willSetInitialBounds = false + + const { screenBounds: prevScreenBounds, insets: prevInsets } = this.getInstanceState() + if (screenBounds.equals(prevScreenBounds) && insets.every((v, i) => v === prevInsets[i])) { + // nothing to do + return this + } + + if (_willSetInitialBounds) { + // If we have just received the initial bounds, don't center the camera. + this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets }) + this.emit('resize', screenBounds.toJson()) + this.setCamera(this.getCamera()) + } else { + if (center && !this.getInstanceState().followingUserId) { + // Get the page center before the change, make the change, and restore it + const before = this.getViewportPageBounds().center + this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets }) + this.emit('resize', screenBounds.toJson()) + this.centerOnPoint(before) + } else { + // Otherwise, + this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets }) + this.emit('resize', screenBounds.toJson()) + this._setCamera(Vec.From({ ...this.getCamera() })) + } + } + + return this + } + + /** + * The bounds of the editor's viewport in screen space. + * + * @public + */ + @computed getViewportScreenBounds() { + const { x, y, w, h } = this.getInstanceState().screenBounds + return new Box(x, y, w, h) + } + + /** + * The center of the editor's viewport in screen space. + * + * @public + */ + @computed getViewportScreenCenter() { + const viewportScreenBounds = this.getViewportScreenBounds() + return new Vec(viewportScreenBounds.w / 2, viewportScreenBounds.h / 2) + } + + /** + * The current viewport in the current page space. + * + * @public + */ + @computed getViewportPageBounds() { + const { w, h } = this.getViewportScreenBounds() + const { x: cx, y: cy, z: cz } = this.getCamera() + return new Box(-cx, -cy, w / cz, h / cz) + } + + /** + * Convert a point in screen space to a point in the current page space. + * + * @example + * ```ts + * editor.screenToPage({ x: 100, y: 100 }) + * ``` + * + * @param point - The point in screen space. + * + * @public + */ + screenToPage(point: VecLike) { + const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + const { x: cx, y: cy, z: cz = 1 } = this.getCamera() + return new Vec( + (point.x - screenBounds.x) / cz - cx, + (point.y - screenBounds.y) / cz - cy, + point.z ?? 0.5 + ) + } + + /** + * Convert a point in the current page space to a point in current screen space. + * + * @example + * ```ts + * editor.pageToScreen({ x: 100, y: 100 }) + * ``` + * + * @param point - The point in page space. + * + * @public + */ + pageToScreen(point: VecLike) { + const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + const { x: cx, y: cy, z: cz = 1 } = this.getCamera() + return new Vec( + (point.x + cx) * cz + screenBounds.x, + (point.y + cy) * cz + screenBounds.y, + point.z ?? 0.5 + ) + } + + /** + * Convert a point in the current page space to a point in current viewport space. + * + * @example + * ```ts + * editor.pageToViewport({ x: 100, y: 100 }) + * ``` + * + * @param point - The point in page space. + * + * @public + */ + pageToViewport(point: VecLike) { + const { x: cx, y: cy, z: cz = 1 } = this.getCamera() + return new Vec((point.x + cx) * cz, (point.y + cy) * cz, point.z ?? 0.5) + } + // Collaborators + + /** + * Returns a list of presence records for all peer collaborators. + * This will return the latest presence record for each connected user. + * + * Convenience wrapper for {@link CollaboratorsManager.getCollaborators}. + * + * @public + */ + getCollaborators() { + return this.collaborators.getCollaborators() + } + + /** + * Returns a list of presence records for all peer collaborators on the current page. + * This will return the latest presence record for each connected user. + * + * Convenience wrapper for {@link CollaboratorsManager.getCollaboratorsOnCurrentPage}. + * + * @public + */ + getCollaboratorsOnCurrentPage() { + return this.collaborators.getCollaboratorsOnCurrentPage() + } + + /** + * Returns a list of presence records for peer collaborators who should currently be + * shown in the UI. Filters {@link Editor.getCollaborators} by activity state + * (active / idle / inactive) and visibility rules such as following and highlighted + * users. Re-evaluates on the collaborator visibility clock, so callers don't need to + * drive their own activity timer. + * + * Convenience wrapper for {@link CollaboratorsManager.getVisibleCollaborators}. + * + * @public + */ + getVisibleCollaborators() { + return this.collaborators.getVisibleCollaborators() + } + + /** + * Returns a list of presence records for peer collaborators who should currently be + * shown in the UI, filtered to those on the current page. + * + * Convenience wrapper for {@link CollaboratorsManager.getVisibleCollaboratorsOnCurrentPage}. + * + * @public + */ + getVisibleCollaboratorsOnCurrentPage() { + return this.collaborators.getVisibleCollaboratorsOnCurrentPage() + } + + // Attribution + + /** + * Get the current user's ID for attribution purposes. + * Also ensures a `user:` record exists in the store for the current user. + * Returns `null` when the user store has no current user. + * + * @public + */ + getAttributionUserId(): string | null { + const user = this.store.props.users.currentUser.get() + if (!user) return null + this._ensureUserRecord(user) + return UserRecordType.parseId(user.id) + } + + /** + * Ensure a user record exists in the store for the given user, + * updating it if the data has changed. + * + * @internal + */ + _ensureUserRecord(user: TLUser): void { + const existing = this.store.get(user.id) + if ( + existing && + existing.name === user.name && + existing.color === user.color && + existing.imageUrl === user.imageUrl && + existing.meta === user.meta + ) { + return + } + this.run( + () => { + this.store.put([user]) + }, + { history: 'ignore' } + ) + } + + /** + * Resolve a display name for a user ID. Asks the + * {@link @tldraw/tlschema#TLUserStore} first (the app's source of truth), + * falling back to the `user:` record in the store. + * + * @public + */ + getAttributionDisplayName(userId: string | null): string | null { + if (!userId) return null + return ( + this.store.props.users.resolve(userId).get()?.name ?? + this.store.get(createUserId(userId))?.name ?? + null + ) + } + + /** + * Resolve a user record by ID. Asks the + * {@link @tldraw/tlschema#TLUserStore} first (the app's source of truth), + * falling back to the `user:` record in the store. + * + * @public + */ + getAttributionUser(userId: string | null): TLUser | null { + if (!userId) return null + return ( + this.store.props.users.resolve(userId).get() ?? this.store.get(createUserId(userId)) ?? null + ) + } + + /** + * Collect user IDs referenced by a set of shapes via shape-specific props + * (e.g. `textLastEditedBy` on notes). + * + * @internal + */ + _getReferencedUserIds(shapes: TLShape[]): Set { + const userIds = new Set() + for (const shape of shapes) { + const util = this.getShapeUtil(shape) + for (const id of util.getReferencedUserIds(shape)) { + userIds.add(id) + } + } + return userIds + } + + // Following + + // When we are 'locked on' to a user, our camera is derived from their camera. + private _isLockedOnFollowingUser = atom('isLockedOnFollowingUser', false) + + /** + * Start viewport-following a user. + * + * @example + * ```ts + * editor.startFollowingUser(myUserId) + * ``` + * + * @param userId - The id of the user to follow. + * + * @public + */ + startFollowingUser(userId: TLUserId): this { + // if we were already following someone, stop following them + this.stopFollowingUser() + + const thisUserId = this.user.getExternalId() + + if (!thisUserId) { + console.warn('You should set the userId for the current instance before following a user') + // allow to continue since it's probably fine most of the time. + } + + const leaderPresence = this._getFollowingPresence(userId) + + if (!leaderPresence) { + return this + } + + const latestLeaderPresence = computed('latestLeaderPresence', () => { + return this._getFollowingPresence(userId) + }) + + transact(() => { + this.updateInstanceState({ followingUserId: userId }, { history: 'ignore' }) + + // we listen for page changes separately from the 'moveTowardsUser' tick + const dispose = react('update current page', () => { + const leaderPresence = latestLeaderPresence.get() + if (!leaderPresence) { + this.stopFollowingUser() + return + } + if ( + leaderPresence.currentPageId !== this.getCurrentPageId() && + this.getPage(leaderPresence.currentPageId) + ) { + // if the page changed, switch page + this.run( + () => { + // sneaky store.put here, we can't go through setCurrentPage because it calls stopFollowingUser + this.store.put([ + { ...this.getInstanceState(), currentPageId: leaderPresence.currentPageId }, + ]) + this._isLockedOnFollowingUser.set(true) + }, + { history: 'ignore' } + ) + } + }) + + const cancel = () => { + dispose() + this._isLockedOnFollowingUser.set(false) + this.off('frame', moveTowardsUser) + this.off('stop-following', cancel) + } + + const moveTowardsUser = () => { + // Stop following if we can't find the user + const leaderPresence = latestLeaderPresence.get() + if (!leaderPresence) { + this.stopFollowingUser() + return + } + + if (this._isLockedOnFollowingUser.get()) return + + const animationSpeed = this.user.getAnimationSpeed() + + if (animationSpeed === 0) { + this._isLockedOnFollowingUser.set(true) + return + } + + const targetViewport = this.getViewportPageBoundsForFollowing() + if (!targetViewport) { + this.stopFollowingUser() + return + } + const currentViewport = this.getViewportPageBounds() + + const diffX = + Math.abs(targetViewport.minX - currentViewport.minX) + + Math.abs(targetViewport.maxX - currentViewport.maxX) + const diffY = + Math.abs(targetViewport.minY - currentViewport.minY) + + Math.abs(targetViewport.maxY - currentViewport.maxY) + + // Stop chasing if we're close enough! + if ( + diffX < this.options.followChaseViewportSnap && + diffY < this.options.followChaseViewportSnap + ) { + this._isLockedOnFollowingUser.set(true) + return + } + + // Chase the user's viewport! + // Interpolate between the current viewport and the target viewport based on animation speed. + // This will produce an 'ease-out' effect. + const t = clamp(animationSpeed * 0.5, 0.1, 0.8) + + const nextViewport = new Box( + lerp(currentViewport.minX, targetViewport.minX, t), + lerp(currentViewport.minY, targetViewport.minY, t), + lerp(currentViewport.width, targetViewport.width, t), + lerp(currentViewport.height, targetViewport.height, t) + ) + + const nextCamera = new Vec( + -nextViewport.x, + -nextViewport.y, + this.getViewportScreenBounds().width / nextViewport.width + ) + + // Update the camera! + this.stopCameraAnimation() + this._setCamera(nextCamera) + } + + this.once('stop-following', cancel) + this.addListener('frame', moveTowardsUser) + + // call once to start synchronously + moveTowardsUser() + }) + + return this + } + + /** + * Stop viewport-following a user. + * + * @example + * ```ts + * editor.stopFollowingUser() + * ``` + * @public + */ + stopFollowingUser(): this { + this.run( + () => { + // commit the current camera to the store + this.store.put([this.getCamera()]) + // this must happen after the camera is committed + this._isLockedOnFollowingUser.set(false) + this.updateInstanceState({ followingUserId: null }) + this.emit('stop-following') + }, + { history: 'ignore' } + ) + return this + } + + /** @internal */ + getUnorderedRenderingShapes( + // The rendering state. We use this method both for rendering, which + // is based on other state, and for computing order for SVG export, + // which should work even when things are for example off-screen. + useEditorState: boolean + ): TLRenderingShape[] { + // Here we get the shape as well as any of its children, as well as their + // opacities. If the shape is being erased, and none of its ancestors are + // being erased, then we reduce the opacity of the shape and all of its + // ancestors; but we don't apply this effect more than once among a set + // of descendants so that it does not compound. + + // This is designed to keep all the shapes in a single list which + // allows the DOM nodes to be reused even when they become children + // of other nodes. + + const renderingShapes: TLRenderingShape[] = [] + + let nextIndex = this.options.maxShapesPerPage * 2 + let nextBackgroundIndex = this.options.maxShapesPerPage + + const erasingShapeIds = new Set(this.getErasingShapeIds()) + + const addShapeById = (id: TLShapeId, opacity: number, isAncestorErasing: boolean) => { + const shape = this.getShape(id) + if (!shape) return + + if (this.isShapeHidden(shape)) { + // process children just in case they are overriding the hidden state + const isErasing = isAncestorErasing || erasingShapeIds.has(id) + for (const childId of this.getSortedChildIdsForParent(id)) { + addShapeById(childId, opacity, isErasing) + } + return + } + + opacity *= shape.opacity + let isShapeErasing = false + const util = this.getShapeUtil(shape) + + if (useEditorState) { + isShapeErasing = !isAncestorErasing && erasingShapeIds.has(id) + if (isShapeErasing) { + opacity *= 0.32 + } + } + + renderingShapes.push({ + id, + shape, + util, + index: nextIndex, + backgroundIndex: nextBackgroundIndex, + opacity, + }) + + nextIndex += 1 + nextBackgroundIndex += 1 + + const childIds = this.getSortedChildIdsForParent(id) + if (!childIds.length) return + + let backgroundIndexToRestore = null + if (util.providesBackgroundForChildren(shape)) { + backgroundIndexToRestore = nextBackgroundIndex + nextBackgroundIndex = nextIndex + nextIndex += this.options.maxShapesPerPage + } + + for (const childId of childIds) { + addShapeById(childId, opacity, isAncestorErasing || isShapeErasing) + } + + if (backgroundIndexToRestore !== null) { + nextBackgroundIndex = backgroundIndexToRestore + } + } + + // If we're using editor state, then we're only interested in on-screen shapes. + // If we're not using the editor state, then we're interested in ALL shapes, even those from other pages. + const pages = useEditorState ? [this.getCurrentPage()] : this.getPages() + for (const page of pages) { + for (const childId of this.getSortedChildIdsForParent(page.id)) { + addShapeById(childId, 1, false) + } + } + + return renderingShapes + } + + // Camera state + // Camera state does two things: first, it allows us to subscribe to whether + // the camera is moving or not; and second, it allows us to update the rendering + // shapes on the canvas. Changing the rendering shapes may cause shapes to + // unmount / remount in the DOM, which is expensive; and computing visibility is + // also expensive in large projects. For this reason, we use a second bounding + // box just for rendering, and we only update after the camera stops moving. + private _cameraStateTimeoutRemaining = 0 + private _decayCameraStateTimeout(elapsed: number) { + this._cameraStateTimeoutRemaining -= elapsed + if (this._cameraStateTimeoutRemaining > 0) return + this.off('tick', this._decayCameraStateTimeout) + this._setCameraState('idle') + } + private _tickCameraState() { + // always reset the timeout + this._cameraStateTimeoutRemaining = this.options.cameraMovingTimeoutMs + // If the state is idle, then start the tick + if (this.getInstanceState().cameraState !== 'idle') return + this._setCameraState('moving') + this._debouncedZoomLevel.set(unsafe__withoutCapture(() => this.getCamera().z)) + this.on('tick', this._decayCameraStateTimeout) + } + private _setCameraState(cameraState: 'idle' | 'moving') { + this.updateInstanceState({ cameraState }, { history: 'ignore' }) + } + + /** + * Whether the camera is moving or idle. + * + * @example + * ```ts + * editor.getCameraState() + * ``` + * + * @public + */ + getCameraState() { + return this.getInstanceState().cameraState + } + + /** + * Get the shapes that should be displayed in the current viewport. + * + * @example + * ```ts + * editor.getRenderingShapes() + * ``` + * + * @public + */ + @computed getRenderingShapes() { + const renderingShapes = this.getUnorderedRenderingShapes(true) + + // Its IMPORTANT that the result be sorted by id AND include the index + // that the shape should be displayed at. Steve, this is the past you + // telling the present you not to change this. + + // We want to sort by id because moving elements about in the DOM will + // cause the element to get removed by react as it moves the DOM node. This + // causes to re-render which is hella annoying and a perf + // drain. By always sorting by 'id' we keep the shapes always in the + // same order; but we later use index to set the element's 'z-index' + // to change the "rendered" position in z-space. + + // For small N, native Array.sort is fast enough that the cache + // bookkeeping is a net loss. Only use the permutation cache when + // there are enough shapes for sort cost to matter. + if (renderingShapes.length <= RENDERING_SHAPES_SORT_CACHE_THRESHOLD) { + this._renderingShapesSortCache = null + return renderingShapes.sort(sortById) + } + + // Sort permutation cache: when the set of ids on the page doesn't + // change (e.g. while drawing a stroke, only props change), we can + // reuse the previous sorted order and place each entry at its known + // sorted position in O(N) instead of running Array.sort O(N log N). + const cache = this._renderingShapesSortCache + if (cache !== null && cache.size === renderingShapes.length) { + const sorted = new Array(renderingShapes.length) + let allMatched = true + for (let i = 0; i < renderingShapes.length; i++) { + const entry = renderingShapes[i] + const pos = cache.get(entry.id) + if (pos === undefined) { + allMatched = false + break + } + sorted[pos] = entry + } + if (allMatched) return sorted + } + + // Slow path: full sort, then cache the permutation by id. + renderingShapes.sort(sortById) + const positionById = new Map() + for (let i = 0; i < renderingShapes.length; i++) { + positionById.set(renderingShapes[i].id, i) + } + this._renderingShapesSortCache = positionById + return renderingShapes + } + + private _renderingShapesSortCache: Map | null = null + + /* --------------------- Pages ---------------------- */ + + @computed private _getAllPagesQuery() { + return this.store.query.records('page') + } + + /** + * Info about the project's current pages. + * + * @example + * ```ts + * editor.getPages() + * ``` + * + * @public + */ + @computed getPages(): TLPage[] { + return Array.from(this._getAllPagesQuery().get()).sort(sortByIndex) + } + + /** + * The current page. + * + * @example + * ```ts + * editor.getCurrentPage() + * ``` + * + * @public + */ + getCurrentPage(): TLPage { + return this.getPage(this.getCurrentPageId())! + } + + /** + * The current page id. + * + * @example + * ```ts + * editor.getCurrentPageId() + * ``` + * + * @public + */ + @computed getCurrentPageId(): TLPageId { + return this.getInstanceState().currentPageId + } + + /** + * Get a page. + * + * @example + * ```ts + * editor.getPage(myPage.id) + * editor.getPage(myPage) + * ``` + * + * @param page - The page (or the page id) to get. + * + * @public + */ + getPage(page: TLPageId | TLPage): TLPage | undefined { + return this.store.get(typeof page === 'string' ? page : page.id) + } + + /* @internal */ + private readonly _currentPageShapeIds: ReturnType + + /** + * An array of all of the shapes on the current page. + * + * @example + * ```ts + * editor.getCurrentPageIds() + * ``` + * + * @public + */ + getCurrentPageShapeIds() { + return this._currentPageShapeIds.get() + } + + /** + * @internal + */ + @computed + getCurrentPageShapeIdsSorted() { + return Array.from(this.getCurrentPageShapeIds()).sort() + } + + /** + * Get the ids of shapes on a page. + * + * @example + * ```ts + * const idsOnPage1 = editor.getPageShapeIds('page1') + * const idsOnPage2 = editor.getPageShapeIds(myPage2) + * ``` + * + * @param page - The page (or the page id) to get the shape ids for. + * + * @public + **/ + getPageShapeIds(page: TLPageId | TLPage): Set { + const pageId = typeof page === 'string' ? page : page.id + const result = this.store.query.exec('shape', { parentId: { eq: pageId } }) + return this.getShapeAndDescendantIds(result.map((s) => s.id)) + } + + /** + * Set the current page. + * + * @example + * ```ts + * editor.setCurrentPage('page1') + * editor.setCurrentPage(myPage1) + * ``` + * + * @param page - The page (or the page id) to set as the current page. + * + * @public + */ + setCurrentPage(page: TLPageId | TLPage): this { + const pageId = typeof page === 'string' ? page : page.id + if (!this.store.has(pageId)) { + console.error("Tried to set the current page id to a page that doesn't exist.") + return this + } + + this.stopFollowingUser() + // finish off any in-progress interactions + this.complete() + + return this.run( + () => { + this.store.put([{ ...this.getInstanceState(), currentPageId: pageId }]) + // ensure camera constraints are applied + this.setCamera(this.getCamera()) + }, + { history: 'record-preserveRedoStack' } + ) + } + + /** + * Update a page. + * + * @example + * ```ts + * editor.updatePage({ id: 'page2', name: 'Page 2' }) + * ``` + * + * @param partial - The partial of the shape to update. + * + * @public + */ + updatePage(partial: RequiredKeys, 'id'>): this { + if (this.getIsReadonly()) return this + + const prev = this.getPage(partial.id) + if (!prev) return this + + return this.run(() => this.store.update(partial.id, (page) => ({ ...page, ...partial }))) + } + + /** + * Create a page whilst ensuring that the page name is unique. + * + * @example + * ```ts + * editor.createPage(myPage) + * editor.createPage({ name: 'Page 2' }) + * ``` + * + * @param page - The page (or page partial) to create. + * + * @public + */ + createPage(page: Partial): this { + this.run(() => { + if (this.getIsReadonly()) return + if (this.getPages().length >= this.options.maxPages) return + const pages = this.getPages() + + const name = getIncrementedName( + page.name ?? 'Page 1', + pages.map((p) => p.name) + ) + + let index = page.index + + if (!index || pages.some((p) => p.index === index)) { + index = getIndexAbove(pages[pages.length - 1].index) + } + + const newPage = PageRecordType.create({ + meta: {}, + ...page, + name, + index, + }) + + this.store.put([newPage]) + }) + return this + } + + /** + * Delete a page. + * + * @example + * ```ts + * editor.deletePage('page1') + * ``` + * + * @param page - The page (or the page id) to delete. + * + * @public + */ + deletePage(page: TLPageId | TLPage): this { + const id = typeof page === 'string' ? page : page.id + this.run( + () => { + if (this.getIsReadonly()) return + const pages = this.getPages() + if (pages.length === 1) return + + const deletedPage = this.getPage(id) + if (!deletedPage) return + + if (id === this.getCurrentPageId()) { + const index = pages.findIndex((page) => page.id === id) + const next = pages[index - 1] ?? pages[index + 1] + this.setCurrentPage(next.id) + } + + const shapes = this.getSortedChildIdsForParent(deletedPage.id) + this.deleteShapes(shapes) + + this.store.remove([deletedPage.id]) + }, + { ignoreShapeLock: true } + ) + return this + } + + /** + * Duplicate a page. + * + * @param page - The page (or the page id) to duplicate. Defaults to the current page. + * @param createId - The id of the new page. Defaults to a new id. + * + * @public + */ + duplicatePage(page: TLPageId | TLPage, createId: TLPageId = PageRecordType.createId()): this { + if (this.getPages().length >= this.options.maxPages) return this + const id = typeof page === 'string' ? page : page.id + const freshPage = this.getPage(id) // get the most recent version of the page anyway + if (!freshPage) return this + + const prevCamera = { ...this.getCamera() } + const content = this.getContentFromCurrentPage(this.getSortedChildIdsForParent(freshPage.id)) + + this.run(() => { + const pages = this.getPages() + const index = getIndexBetween(freshPage.index, pages[pages.indexOf(freshPage) + 1]?.index) + + // create the page (also creates the pagestate and camera for the new page) + this.createPage({ name: freshPage.name + ' Copy', id: createId, index }) + // set the new page as the current page + this.setCurrentPage(createId) + // update the new page's camera to the previous page's camera + this.setCamera(prevCamera) + + if (content) { + // If we had content on the previous page, put it on the new page + return this.putContentOntoCurrentPage(content) + } + }) + + return this + } + + /** + * Rename a page. + * + * @example + * ```ts + * editor.renamePage('page1', 'My Page') + * ``` + * + * @param page - The page (or the page id) to rename. + * @param name - The new name. + * + * @public + */ + renamePage(page: TLPageId | TLPage, name: string) { + const id = typeof page === 'string' ? page : page.id + if (this.getIsReadonly()) return this + this.updatePage({ id, name }) + return this + } + + /* --------------------- Assets --------------------- */ + + /** @internal */ + @computed private _getAllAssetsQuery() { + return this.store.query.records('asset') + } + + /** + * Get all assets in the editor. + * + * @public + */ + getAssets() { + return this._getAllAssetsQuery().get() + } + + /** + * Create one or more assets. + * + * @example + * ```ts + * editor.createAssets([...myAssets]) + * ``` + * + * @param assets - The assets to create. + * + * @public + */ + createAssets(assets: TLAsset[]): this { + if (this.getIsReadonly()) return this + if (assets.length <= 0) return this + this.run(() => this.store.put(assets), { history: 'ignore' }) + return this + } + + /** + * Update one or more assets. + * + * @example + * ```ts + * editor.updateAssets([{ id: 'asset1', name: 'New name' }]) + * ``` + * + * @param assets - The assets to update. + * + * @public + */ + updateAssets(assets: TLAssetPartial[]): this { + if (this.getIsReadonly()) return this + if (assets.length <= 0) return this + this.run( + () => { + this.store.put( + assets.map((partial) => ({ + ...this.store.get(partial.id)!, + ...partial, + })) + ) + }, + { history: 'ignore' } + ) + return this + } + + /** + * Delete one or more assets. + * + * @example + * ```ts + * editor.deleteAssets(['asset1', 'asset2']) + * ``` + * + * @param assets - The assets (or asset ids) to delete. + * + * @public + */ + deleteAssets(assets: TLAssetId[] | TLAsset[]): this { + if (this.getIsReadonly()) return this + + const ids = + typeof assets[0] === 'string' + ? (assets as TLAssetId[]) + : (assets as TLAsset[]).map((a) => a.id) + if (ids.length <= 0) return this + + this.run( + () => { + // the asset store's remove is async; surface failures instead of leaving an unhandled rejection + Promise.resolve(this.store.props.assets.remove?.(ids)).catch((err) => + console.error('Error while removing assets from the asset store:', err) + ) + this.store.remove(ids) + }, + { history: 'ignore' } + ) + return this + } + + /** + * Get an asset by its id. + * + * @example + * ```ts + * editor.getAsset('asset1') + * ``` + * + * @param asset - The asset (or asset id) to get. + * + * @public + */ + getAsset(asset: T | T['id']): T | undefined { + return this.store.get(typeof asset === 'string' ? asset : asset.id) as T | undefined + } + + async resolveAssetUrl( + assetId: TLAssetId | null, + context: { + screenScale?: number + shouldResolveToOriginal?: boolean + dpr?: number + } + ): Promise { + if (!assetId) return null + const asset = this.getAsset(assetId) + if (!asset) return null + + const { + screenScale = 1, + shouldResolveToOriginal = false, + dpr = this.getInstanceState().devicePixelRatio, + } = context + + // We only look at the zoom level at powers of 2. + const zoomStepFunction = (zoom: number) => Math.pow(2, Math.ceil(Math.log2(zoom))) + const steppedScreenScale = zoomStepFunction(screenScale) + const networkEffectiveType: string | null = + 'connection' in navigator ? ((navigator as any).connection?.effectiveType ?? null) : null + + return await this.store.props.assets.resolve(asset, { + screenScale: screenScale || 1, + steppedScreenScale, + dpr, + networkEffectiveType, + shouldResolveToOriginal, + }) + } + /** + * Upload an asset to the store's asset service, returning a URL that can be used to resolve the + * asset. + */ + async uploadAsset( + asset: TLAsset, + file: File, + abortSignal?: AbortSignal + ): Promise<{ src: string; meta?: JsonObject }> { + return await this.store.props.assets.upload(asset, file, abortSignal) + } + + /* --------------------- Shapes --------------------- */ + + private _shapeGeometryCaches: Record> = {} + + /** + * Get the geometry of a shape in shape-space. + * + * @example + * ```ts + * editor.getShapeGeometry(myShape) + * editor.getShapeGeometry(myShapeId) + * editor.getShapeGeometry(myShapeId, { context: "arrow" }) + * ``` + * + * @param shape - The shape (or shape id) to get the geometry for. + * @param opts - Additional options about the request for geometry. Passed to {@link ShapeUtil.getGeometry}. + * + * @public + */ + getShapeGeometry(shape: TLShape | TLShapeId, opts?: TLGeometryOpts): T { + const context = opts?.context ?? 'none' + if (!this._shapeGeometryCaches[context]) { + this._shapeGeometryCaches[context] = this.store.createComputedCache( + 'bounds', + (shape) => { + this.fonts.trackFontsForShape(shape) + return this.getShapeUtil(shape).getGeometry(shape, opts) + }, + { areRecordsEqual: areShapesContentEqual } + ) + } + return this._shapeGeometryCaches[context].get( + typeof shape === 'string' ? shape : shape.id + )! as T + } + + /** @internal */ + @computed private _getShapeHandlesCache(): ComputedCache { + return this.store.createComputedCache( + 'handles', + (shape) => { + return this.getShapeUtil(shape).getHandles?.(shape) + }, + { + areRecordsEqual: areShapesContentEqual, + } + ) + } + + /** + * Get the handles (if any) for a shape. + * + * @example + * ```ts + * editor.getShapeHandles(myShape) + * editor.getShapeHandles(myShapeId) + * ``` + * + * @param shape - The shape (or shape id) to get the handles for. + * @public + */ + getShapeHandles(shape: T | T['id']): TLHandle[] | undefined { + return this._getShapeHandlesCache().get(typeof shape === 'string' ? shape : shape.id) + } + + /** + * Get the local transform for a shape as a matrix model. This transform reflects both its + * translation (x, y) from from either its parent's top left corner, if the shape's parent is + * another shape, or else from the 0,0 of the page, if the shape's parent is the page; and the + * shape's rotation. + * + * @example + * ```ts + * editor.getShapeLocalTransform(myShape) + * ``` + * + * @param shape - The shape to get the local transform for. + * + * @public + */ + getShapeLocalTransform(shape: TLShape | TLShapeId): Mat { + const id = typeof shape === 'string' ? shape : shape.id + const freshShape = this.getShape(id) + if (!freshShape) throw Error('Editor.getTransform: shape not found') + return Mat.Identity().translate(freshShape.x, freshShape.y).rotate(freshShape.rotation) + } + + /** + * A cache of page transforms. + * + * @internal + */ + @computed private _getShapePageTransformCache(): ComputedCache { + return this.store.createComputedCache('pageTransformCache', (shape) => { + if (isPageId(shape.parentId)) { + return this.getShapeLocalTransform(shape) + } + + // If the shape's parent doesn't exist yet (e.g. when merging in changes from remote in the wrong order) + // then we can't compute the transform yet, so just return the identity matrix. + // In the future we should look at creating a store update mechanism that understands and preserves + // ordering. + const parentTransform = + this._getShapePageTransformCache().get(shape.parentId) ?? Mat.Identity() + return Mat.Compose(parentTransform, this.getShapeLocalTransform(shape)!) + }) + } + + /** + * Get the local transform of a shape's parent as a matrix model. + * + * @example + * ```ts + * editor.getShapeParentTransform(myShape) + * ``` + * + * @param shape - The shape (or shape id) to get the parent transform for. + * + * @public + */ + getShapeParentTransform(shape: TLShape | TLShapeId): Mat { + const id = typeof shape === 'string' ? shape : shape.id + const freshShape = this.getShape(id) + if (!freshShape || isPageId(freshShape.parentId)) return Mat.Identity() + return this._getShapePageTransformCache().get(freshShape.parentId) ?? Mat.Identity() + } + + /** + * Get the transform of a shape in the current page space. + * + * @example + * ```ts + * editor.getShapePageTransform(myShape) + * editor.getShapePageTransform(myShapeId) + * ``` + * + * @param shape - The shape (or shape id) to get the page transform for. + * + * @public + */ + getShapePageTransform(shape: TLShape | TLShapeId): Mat { + const id = typeof shape === 'string' ? shape : shape.id + return this._getShapePageTransformCache().get(id) ?? Mat.Identity() + } + + /** @internal */ + @computed private _getShapePageBoundsCache(): ComputedCache { + return this.store.createComputedCache('pageBoundsCache', (shape) => { + return Box.FromPoints( + this.getShapePageTransform(shape).applyToPoints(this.getShapeGeometry(shape).boundsVertices) + ) + }) + } + + /** + * Get the bounds of a shape in the current page space. + * + * @example + * ```ts + * editor.getShapePageBounds(myShape) + * editor.getShapePageBounds(myShapeId) + * ``` + * + * @param shape - The shape (or shape id) to get the bounds for. + * + * @public + */ + getShapePageBounds(shape: TLShape | TLShapeId): Box | undefined { + return this._getShapePageBoundsCache().get(typeof shape === 'string' ? shape : shape.id) + } + + /** + * A cache of clip paths used for clipping. + * + * @internal + */ + @computed private _getShapeClipPathCache(): ComputedCache { + return this.store.createComputedCache('clipPathCache', (shape) => { + const pageMask = this._getShapeMaskCache().get(shape.id) + if (!pageMask) return undefined + if (pageMask.length === 0) { + return `polygon(0px 0px, 0px 0px, 0px 0px)` + } + + const pageTransform = this._getShapePageTransformCache().get(shape.id) + if (!pageTransform) return undefined + + const localMask = Mat.applyToPoints(Mat.Inverse(pageTransform), pageMask) + + return `polygon(${localMask.map((p) => `${p.x}px ${p.y}px`).join(',')})` + }) + } + + /** + * Get the clip path for a shape. + * + * @example + * ```ts + * const clipPath = editor.getShapeClipPath(shape) + * const clipPath = editor.getShapeClipPath(shape.id) + * ``` + * + * @param shape - The shape (or shape id) to get the clip path for. + * + * @returns The clip path or undefined. + * + * @public + */ + getShapeClipPath(shape: TLShape | TLShapeId): string | undefined { + return this._getShapeClipPathCache().get(typeof shape === 'string' ? shape : shape.id) + } + + /** @internal */ + @computed private _getShapeMaskCache(): ComputedCache { + return this.store.createComputedCache('pageMaskCache', (shape) => { + if (isPageId(shape.parentId)) return undefined + + const clipPaths: Vec[][] = [] + // Get all ancestors that can potentially clip this shape + for (const ancestor of this.getShapeAncestors(shape.id)) { + const util = this.getShapeUtil(ancestor) + const clipPath = util.getClipPath?.(ancestor) + if (!clipPath) continue + if (util.shouldClipChild?.(shape) === false) continue + const pageTransform = this.getShapePageTransform(ancestor.id) + clipPaths.push(pageTransform.applyToPoints(clipPath)) + } + if (clipPaths.length === 0) return undefined + + const pageMask = clipPaths.reduce((acc, b) => { + const intersection = intersectPolygonPolygon(acc, b) + if (intersection) { + return intersection.map(Vec.Cast) + } + return [] + }) + + return pageMask + }) + } + + /** + * Get the mask (in the current page space) for a shape. + * + * @example + * ```ts + * const pageMask = editor.getShapeMask(shape.id) + * ``` + * + * @param shape - The shape (or the shape id) of the shape to get the mask for. + * + * @returns The mask for the shape. + * + * @public + */ + getShapeMask(shape: TLShapeId | TLShape): VecLike[] | undefined { + return this._getShapeMaskCache().get(typeof shape === 'string' ? shape : shape.id) + } + + /** + * Get the bounds of a shape in the current page space, incorporating any masks. For example, if the + * shape were the child of a frame and was half way out of the frame, the bounds would be the half + * of the shape that was in the frame. + * + * @example + * ```ts + * editor.getShapeMaskedPageBounds(myShape) + * editor.getShapeMaskedPageBounds(myShapeId) + * ``` + * + * @param shape - The shape to get the masked bounds for. + * + * @public + */ + getShapeMaskedPageBounds(shape: TLShapeId | TLShape): Box | undefined { + if (typeof shape !== 'string') shape = shape.id + return this._getShapeMaskedPageBoundsCache().get(shape) + } + + /** @internal */ + @computed private _getShapeMaskedPageBoundsCache(): ComputedCache { + return this.store.createComputedCache('shapeMaskedPageBoundsCache', (shape) => { + const pageBounds = this._getShapePageBoundsCache().get(shape.id) + if (!pageBounds) return + const pageMask = this._getShapeMaskCache().get(shape.id) + if (pageMask) { + if (pageMask.length === 0) return undefined + const { corners } = pageBounds + // the mask may have fewer than four points (e.g. a triangular clip path) + if ( + pageMask.length === corners.length && + corners.every((p, i) => Vec.Equals(p, pageMask[i])) + ) { + return pageBounds.clone() + } + const intersection = intersectPolygonPolygon(pageMask, corners) + if (!intersection) return + return Box.FromPoints(intersection) + } + return pageBounds + }) + } + + /** + * Get the ancestors of a shape. + * + * @example + * ```ts + * const ancestors = editor.getShapeAncestors(myShape) + * const ancestors = editor.getShapeAncestors(myShapeId) + * ``` + * + * @param shape - The shape (or shape id) to get the ancestors for. + * @param acc - The accumulator. + * + * @public + */ + getShapeAncestors(shape: TLShapeId | TLShape, acc: TLShape[] = []): TLShape[] { + const id = typeof shape === 'string' ? shape : shape.id + const freshShape = this.getShape(id) + if (!freshShape) return acc + const parentId = freshShape.parentId + if (isPageId(parentId)) { + acc.reverse() + return acc + } + + const parent = this.store.get(parentId) + if (!parent) return acc + acc.push(parent) + return this.getShapeAncestors(parent, acc) + } + + /** + * Find the first ancestor matching the given predicate + * + * @example + * ```ts + * const ancestor = editor.findShapeAncestor(myShape) + * const ancestor = editor.findShapeAncestor(myShape.id) + * const ancestor = editor.findShapeAncestor(myShape.id, (shape) => shape.type === 'frame') + * ``` + * + * @param shape - The shape to check the ancestors for. + * @param predicate - The predicate to match. + * + * @public + */ + findShapeAncestor( + shape: TLShape | TLShapeId, + predicate: (parent: TLShape) => boolean + ): TLShape | undefined { + const id = typeof shape === 'string' ? shape : shape.id + const freshShape = this.getShape(id) + if (!freshShape) return + + const parentId = freshShape.parentId + if (isPageId(parentId)) return + + const parent = this.getShape(parentId) + if (!parent) return + return predicate(parent) ? parent : this.findShapeAncestor(parent, predicate) + } + + /** + * Returns true if the the given shape has the given ancestor. + * + * @param shape - The shape. + * @param ancestorId - The id of the ancestor. + * + * @public + */ + hasAncestor(shape: TLShape | TLShapeId | undefined, ancestorId: TLShapeId): boolean { + const id = typeof shape === 'string' ? shape : shape?.id + const freshShape = id && this.getShape(id) + if (!freshShape) return false + if (freshShape.parentId === ancestorId) return true + return this.hasAncestor(this.getShapeParent(freshShape), ancestorId) + } + + /** + * Get the common ancestor of two or more shapes that matches a predicate. + * + * @param shapes - The shapes (or shape ids) to check. + * @param predicate - The predicate to match. + */ + findCommonAncestor( + shapes: TLShape[] | TLShapeId[], + predicate?: (shape: TLShape) => boolean + ): TLShapeId | undefined { + if (shapes.length === 0) { + return + } + + const ids = toShapeIds(shapes) + const freshShapes = compact(ids.map((id) => this.getShape(id))) + + if (freshShapes.length === 1) { + const parentId = freshShapes[0].parentId + if (isPageId(parentId)) { + return + } + return predicate ? this.findShapeAncestor(freshShapes[0], predicate)?.id : parentId + } + + const [nodeA, ...others] = freshShapes + let ancestor = this.getShapeParent(nodeA) + while (ancestor) { + // TODO: this is not ideal, optimize + if (predicate && !predicate(ancestor)) { + ancestor = this.getShapeParent(ancestor) + continue + } + if (others.every((shape) => this.hasAncestor(shape, ancestor!.id))) { + return ancestor!.id + } + ancestor = this.getShapeParent(ancestor) + } + return undefined + } + + /** + * Check whether a shape or its parent is locked. + * + * @param shape - The shape (or shape id) to check. + * + * @public + */ + isShapeOrAncestorLocked(shape?: TLShape | TLShapeId): boolean { + const _shape = shape && this.getShape(shape) + if (_shape === undefined) return false + if (_shape.isLocked) return true + return this.isShapeOrAncestorLocked(this.getShapeParent(_shape)) + } + + /** + * Get shapes that are outside of the viewport. + * + * @public + */ + @computed + getNotVisibleShapes() { + return this._notVisibleShapes.get() + } + + private _notVisibleShapes = notVisibleShapes(this) + private _culledShapesCache: Set | null = null + + /** + * Get culled shapes (those that should not render), taking into account which shapes are selected or editing. + * + * @public + */ + @computed + getCulledShapes() { + const notVisibleShapes = this.getNotVisibleShapes() + const selectedShapeIds = this.getSelectedShapeIds() + const editingId = this.getEditingShapeId() + const nextValue = new Set(notVisibleShapes) + // we don't cull the shape we are editing + if (editingId) { + nextValue.delete(editingId) + } + // we also don't cull selected shapes + selectedShapeIds.forEach((id) => { + nextValue.delete(id) + }) + + // Cache optimization: return same Set object if contents unchanged + // This allows consumers to use === comparison and prevents unnecessary re-renders + const prevValue = this._culledShapesCache + if (prevValue) { + // If sizes differ, contents must differ + if (prevValue.size !== nextValue.size) { + this._culledShapesCache = nextValue + return nextValue + } + + // Check if all elements are the same + for (const id of prevValue) { + if (!nextValue.has(id)) { + // Found a difference, update cache and return new set + this._culledShapesCache = nextValue + return nextValue + } + } + + // Loop completed without finding differences - contents identical + return prevValue + } + + this._culledShapesCache = nextValue + return nextValue + } + + /** + * The bounds of the current page (the common bounds of all of the shapes on the page). + * + * @public + */ + @computed getCurrentPageBounds(): Box | undefined { + let commonBounds: Box | undefined + + this.getCurrentPageShapeIdsSorted().forEach((shapeId) => { + if (this.isShapeHidden(shapeId)) return + const bounds = this.getShapeMaskedPageBounds(shapeId) + if (!bounds) return + if (!commonBounds) { + commonBounds = bounds.clone() + } else { + commonBounds = commonBounds.expand(bounds) + } + }) + + return commonBounds + } + + /** + * Get the hit-test margin in page space—the distance in page units within which a pointer is + * considered to be touching a shape. This resolves to {@link TldrawOptions.hitTestMargin} (or + * {@link TldrawOptions.coarseHitTestMargin} when using a coarse pointer) divided by the current + * zoom level, so it stays a constant distance in screen space. + * + * @returns The hit-test margin in page space. + * + * @public + */ + @computed getHitTestMargin(): number { + const { hitTestMargin, coarseHitTestMargin } = this.options + const margin = this.getInstanceState().isCoarsePointer ? coarseHitTestMargin : hitTestMargin + return margin / this.getZoomLevel() + } + + /** + * Get the top-most selected shape at the given point, ignoring groups. + * + * @param point - The point to check. + * + * @returns The top-most selected shape at the given point, or undefined if there is no shape at the point. + */ + getSelectedShapeAtPoint(point: VecLike): TLShape | undefined { + const selectedShapeIds = this.getSelectedShapeIds() + if (selectedShapeIds.length === 0) return undefined + const selectedShapeIdSet = new Set(selectedShapeIds) + const margin = this.getHitTestMargin() + const sortedShapes = this.getCurrentPageShapesSorted() + + // iterate from the top (highest z-index) to find the top-most matching shape + for (let i = sortedShapes.length - 1; i >= 0; i--) { + const shape = sortedShapes[i] + if (shape.type === 'group') continue + if (!selectedShapeIdSet.has(shape.id)) continue + if ( + this.getShapeGeometry(shape).hitTestPoint( + this.getPointInShapeSpace(shape, point), + margin, + true + ) + ) { + return shape + } + } + + return undefined + } + + /** + * Get the shape at the current point. + * + * @param point - The point to check. + * @param opts - Options for the check: `hitInside` to check if the point is inside the shape, `margin` to check if the point is within a margin of the shape, `hitFrameInside` to check if the point is inside the frame, and `filter` to filter the shapes to check. + * + * @returns The shape at the given point, or undefined if there is no shape at the point. + */ + getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined { + const viewportPageBounds = this.getViewportPageBounds() + const { + filter, + margin = 0, + hitLocked = false, + hitLabels = false, + hitInside = false, + hitFrameInside = false, + } = opts + + const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin] + + let inHollowSmallestArea = Infinity + let inHollowSmallestAreaHit: TLShape | null = null + + let inMarginClosestToEdgeDistance = Infinity + let inMarginClosestToEdgeHit: TLShape | null = null + + // Use larger margin for spatial search to account for edge distance checks + const searchMargin = Math.max(innerMargin, outerMargin, this.getHitTestMargin()) + const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin) + + const shapesToCheck = opts.renderingOnly + ? this.getCurrentPageRenderingShapesSorted() + : this.getCurrentPageShapesSorted() + + for (let i = shapesToCheck.length - 1; i >= 0; i--) { + const shape = shapesToCheck[i] + // Frame-like shapes have labels positioned above the shape (outside bounds), so always include them + if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue + if ( + (shape.isLocked && !hitLocked) || + this.isShapeHidden(shape) || + this.isShapeOfType(shape, 'group') + ) { + continue + } + const pageMask = this.getShapeMask(shape) + if (pageMask && !pointInPolygon(point, pageMask)) continue + if (filter && !filter(shape)) continue + + const geometry = this.getShapeGeometry(shape) + const isGroup = geometry instanceof Group2d + + const pointInShapeSpace = this.getPointInShapeSpace(shape, point) + + // Check labels first. Only group geometries can carry a label child; a frame-like + // shape util may still return a plain geometry. + const shapeUtil = this.getShapeUtil(shape) + const isShapeFrameLike = this.isShapeFrameLike(shape) + if ( + isGroup && + (isShapeFrameLike || + ((this.isShapeOfType(shape, 'note') || + this.isShapeOfType(shape, 'arrow') || + (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) && + shapeUtil.getText(shape)?.trim())) + ) { + for (const childGeometry of geometry.children) { + if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) { + return shape + } + } + } + + if (isShapeFrameLike) { + // On the rare case that we've hit a frame-like shape (not its label), test again hitInside to be forced true; + // this prevents clicks from passing through the body of a frame to shapes behind it. + + // If the hit is within the frame's outer margin, then select the frame + const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside) + if ( + hitFrameInside + ? (distance > 0 && distance <= outerMargin) || + (distance <= 0 && distance > -innerMargin) + : distance > 0 && distance <= outerMargin + ) { + return inMarginClosestToEdgeHit || shape + } + + if (geometry.hitTestPoint(pointInShapeSpace, 0, true)) { + // Once we've hit a frame, we want to end the search. If we have hit a shape + // already, then this would either be above the frame or a child of the frame, + // so we want to return that. Otherwise, the point is in the empty space of the + // frame. If `hitFrameInside` is true (e.g. used drawing an arrow into the + // frame) we the frame itself; other wise, (e.g. when hovering or pointing) + // we would want to return null. + return ( + inMarginClosestToEdgeHit || + inHollowSmallestAreaHit || + (hitFrameInside ? shape : undefined) + ) + } + continue + } + + let distance: number + + if (isGroup) { + let minDistance = Infinity + for (const childGeometry of geometry.children) { + if (childGeometry.isLabel && !hitLabels) continue + + // hit test the all of the child geometries that aren't labels + const tDistance = childGeometry.distanceToPoint(pointInShapeSpace, hitInside) + if (tDistance < minDistance) { + minDistance = tDistance + } + } + + distance = minDistance + } else { + // If the margin is zero and the geometry has a very small width or height, + // then check the actual distance. This is to prevent a bug where straight + // lines would never pass the broad phase (point-in-bounds) check. + if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) { + distance = geometry.distanceToPoint(pointInShapeSpace, hitInside) + } else { + // Broad phase + if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) { + // Narrow phase (actual distance) + distance = geometry.distanceToPoint(pointInShapeSpace, hitInside) + } else { + // Failed the broad phase, geddafugaotta'ere! + distance = Infinity + } + } + } + + if (geometry.isClosed) { + // For closed shapes, the distance will be positive if outside of + // the shape or negative if inside of the shape. If the distance + // is greater than the margin, then it's a miss. Otherwise... + + // Are we close to the shape's edge? + if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) { + if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) { + // If the geometry rejects this hit (e.g. transparent image pixel), + // skip this shape and check shapes behind it. + if (geometry.ignoreHit(pointInShapeSpace)) { + continue + } + // If the shape is filled, then it's a hit. Remember, we're + // starting from the TOP-MOST shape in z-index order, so any + // other hits would be occluded by the shape. + return inMarginClosestToEdgeHit || shape + } else { + // If we're close to the edge of the shape, and if it's the closest edge among + // all the edges that we've gotten close to so far, then we will want to hit the + // shape unless we hit something else or closer in later iterations. + if ( + hitInside + ? // On hitInside, the distance will be negative for hits inside + // If the distance is positive, check against the outer margin + (distance > 0 && distance <= outerMargin) || + // If the distance is negative, check against the inner margin + (distance <= 0 && distance > -innerMargin) + : // If hitInside is false, then sadly _we do not know_ whether the + // point is inside or outside of the shape, so we check against + // the max of the two margins + Math.abs(distance) <= Math.max(innerMargin, outerMargin) + ) { + if (Math.abs(distance) < inMarginClosestToEdgeDistance) { + inMarginClosestToEdgeDistance = Math.abs(distance) + inMarginClosestToEdgeHit = shape + } + } else if (!inMarginClosestToEdgeHit) { + // If the shape is bigger than the viewport, then skip it. (Only here: its + // edges should still be hittable within the margin.) + if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue + + // If we're not within margin distance to any edge, and if the + // shape is hollow, then we want to hit the shape with the + // smallest area. (There's a bug here with self-intersecting + // shapes, like a closed drawing of an "8", but that's a bigger + // problem to solve.) + const { area } = geometry + if (area < inHollowSmallestArea) { + inHollowSmallestArea = area + inHollowSmallestAreaHit = shape + } + } + } + } + } else { + // For open shapes (e.g. lines or draw shapes) always use the margin. + // If the distance is less than the margin, return the shape as the hit. + // Use the editor's configurable hit test margin. + if (distance < this.getHitTestMargin()) { + // An edge we already hit (above this shape) that is at least as close still wins, + // matching the closest-edge rule used for hollow shapes + if (inMarginClosestToEdgeHit && inMarginClosestToEdgeDistance <= distance) { + return inMarginClosestToEdgeHit + } + return shape + } + } + } + + // If we haven't hit any filled shapes or frames, then return either + // the shape who we hit within the margin (and of those, the one that + // had the shortest distance between the point and the shape edge), + // or else the hollow shape with the smallest area—or if we didn't hit + // any margins or any hollow shapes, then null. + return inMarginClosestToEdgeHit || inHollowSmallestAreaHit || undefined + } + + /** + * Get the shapes, if any, at a given page point. + * + * @example + * ```ts + * editor.getShapesAtPoint({ x: 100, y: 100 }) + * editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, margin: 8 }) + * ``` + * + * @param point - The page point to test. + * @param opts - The options for the hit point testing. + * + * @returns An array of shapes at the given point, sorted in reverse order of their absolute z-index (top-most shape first). + * + * @public + */ + getShapesAtPoint( + point: VecLike, + opts = {} as { margin?: number; hitInside?: boolean } + ): TLShape[] { + const margin = opts.margin ?? 0 + const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin) + + // Get all page shapes in z-index order and filter to candidates that pass isPointInShape. + // Frame-like shapes are always checked because their labels can be outside their bounds. + // Iterate backwards so the result is pre-sorted in reverse z-index order (top-most first). + const sorted = this.getCurrentPageShapesSorted() + const result: TLShape[] = [] + for (let i = sorted.length - 1; i >= 0; i--) { + const shape = sorted[i] + if (this.isShapeHidden(shape)) continue + if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue + if (this.isPointInShape(shape, point, opts)) result.push(shape) + } + return result + } + + /** + * Get shape IDs within the given bounds. + * + * Note: Uses shape page bounds only. Frames with labels outside their bounds + * may not be included even if the label is within the search bounds. + * + * Note: Results are unordered. If you need z-order, combine with sorted shapes: + * ```ts + * const candidates = editor.getShapeIdsInsideBounds(bounds) + * const sorted = editor.getCurrentPageShapesSorted().filter(s => candidates.has(s.id)) + * ``` + * + * @param bounds - The bounds to search within. + * @returns Unordered set of shape IDs within the given bounds. + * + * @public + */ + getShapeIdsInsideBounds(bounds: Box): Set { + return this._spatialIndex.getShapeIdsInsideBounds(bounds) + } + + /** + * Test whether a point (in the current page space) will will a shape. This method takes into account masks, + * such as when a shape is the child of a frame and is partially clipped by the frame. + * + * @example + * ```ts + * editor.isPointInShape({ x: 100, y: 100 }, myShape) + * ``` + * + * @param shape - The shape to test against. + * @param point - The page point to test (in the current page space). + * @param opts - The options for the hit point testing. + * + * @public + */ + isPointInShape( + shape: TLShape | TLShapeId, + point: VecLike, + opts = {} as { + margin?: number + hitInside?: boolean + } + ): boolean { + const { hitInside = false, margin = 0 } = opts + const id = typeof shape === 'string' ? shape : shape.id + // If the shape is masked, and if the point falls outside of that + // mask, then it's definitely a miss—we don't need to test further. + const pageMask = this.getShapeMask(id) + if (pageMask && !pointInPolygon(point, pageMask)) return false + + return this.getShapeGeometry(id).hitTestPoint( + this.getPointInShapeSpace(shape, point), + margin, + hitInside + ) + } + + /** + * Convert a point in the current page space to a point in the local space of a shape. For example, if a + * shape's page point were `{ x: 100, y: 100 }`, a page point at `{ x: 110, y: 110 }` would be at + * `{ x: 10, y: 10 }` in the shape's local space. + * + * @example + * ```ts + * editor.getPointInShapeSpace(myShape, { x: 100, y: 100 }) + * ``` + * + * @param shape - The shape to get the point in the local space of. + * @param point - The page point to get in the local space of the shape. + * + * @public + */ + getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec { + const id = typeof shape === 'string' ? shape : shape.id + return this._getShapePageTransformCache().get(id)!.clone().invert().applyToPoint(point) + } + + /** + * Convert a delta in the current page space to a point in the local space of a shape's parent. + * + * @example + * ```ts + * editor.getPointInParentSpace(myShape.id, { x: 100, y: 100 }) + * ``` + * + * @param shape - The shape to get the point in the local space of. + * @param point - The page point to get in the local space of the shape. + * + * @public + */ + getPointInParentSpace(shape: TLShapeId | TLShape, point: VecLike): Vec { + const id = typeof shape === 'string' ? shape : shape.id + const freshShape = this.getShape(id) + if (!freshShape) return new Vec(0, 0) + if (isPageId(freshShape.parentId)) return Vec.From(point) + + return this.getShapePageTransform(freshShape.parentId).clone().invert().applyToPoint(point) + } + + /** + * An array containing all of the shapes in the current page. + * + * @public + */ + @computed getCurrentPageShapes(): TLShape[] { + return Array.from(this.getCurrentPageShapeIds(), (id) => this.store.get(id)! as TLShape) + } + + /** + * An array containing all of the shapes in the current page, sorted in z-index order (accounting + * for nested shapes): e.g. A, B, BA, BB, C. + * + * @public + */ + @computed getCurrentPageShapesSorted(): TLShape[] { + const result: TLShape[] = [] + const topLevelShapes = this.getSortedChildIdsForParent(this.getCurrentPageId()) + + for (let i = 0, n = topLevelShapes.length; i < n; i++) { + pushShapeWithDescendants(this, topLevelShapes[i], result) + } + + return result + } + + /** + * An array containing all of the rendering shapes in the current page, sorted in z-index order (accounting + * for nested shapes): e.g. A, B, BA, BB, C. + * + * @public + */ + @computed getCurrentPageRenderingShapesSorted(): TLShape[] { + const culledShapes = this.getCulledShapes() + return this.getCurrentPageShapesSorted().filter( + ({ id }) => !culledShapes.has(id) && !this.isShapeHidden(id) + ) + } + + /** + * Get whether a shape matches the type of a TLShapeUtil. + * + * @example + * ```ts + * const isArrowShape = isShapeOfType(someShape, 'arrow') + * ``` + * + * @param util - the TLShapeUtil constructor to test against + * @param shape - the shape to test + * + * @public + */ + isShapeOfType( + shape: TLShape, + type: K + ): shape is Extract + isShapeOfType( + shape: TLShape, + type: T['type'] + ): shape is Extract + isShapeOfType(shapeId: TLShapeId, type: T['type']): boolean + isShapeOfType(arg: TLShape | TLShapeId, type: TLShape['type']) { + const shape = typeof arg === 'string' ? this.getShape(arg) : arg + if (!shape) return false + return shape.type === type + } + + /** + * Get whether a shape behaves like a frame — a container that has child + * shapes, requires full-brush selection, blocks erasure from inside, etc. + * + * @example + * ```ts + * const isFrameLike = editor.isShapeFrameLike(someShape) + * ``` + * + * @param shape - The shape (or shape id) to test. + * + * @public + */ + isShapeFrameLike(shape: TLShape | TLShapeId): boolean { + const _shape = typeof shape === 'string' ? this.getShape(shape) : shape + if (!_shape) return false + return this.getShapeUtil(_shape).isFrameLike(_shape) + } + + /** + * Get a shape by its id. + * + * @example + * ```ts + * editor.getShape('box1') + * ``` + * + * @param shape - The shape (or the id of the shape) to get. + * + * @public + */ + getShape(shape: TLShape | TLParentId): T | undefined { + const id = typeof shape === 'string' ? shape : shape.id + if (!isShapeId(id)) return undefined + return this.store.get(id) as T + } + + /** + * Get the parent shape for a given shape. Returns undefined if the shape is the direct child of + * the page. + * + * @example + * ```ts + * editor.getShapeParent(myShape) + * ``` + * + * @public + */ + getShapeParent(shape?: TLShape | TLShapeId): TLShape | undefined { + const id = typeof shape === 'string' ? shape : shape?.id + if (!id) return undefined + const freshShape = this.getShape(id) + if (freshShape === undefined || !isShapeId(freshShape.parentId)) return undefined + return this.getShape(freshShape.parentId) + } + + /** + * If siblingShape and targetShape are siblings, this returns targetShape. If targetShape has an + * ancestor who is a sibling of siblingShape, this returns that ancestor. Otherwise, this returns + * undefined. + * + * @internal + */ + getShapeNearestSibling( + siblingShape: TLShape, + targetShape: TLShape | undefined + ): TLShape | undefined { + if (!targetShape) { + return undefined + } + if (targetShape.parentId === siblingShape.parentId) { + return targetShape + } + + const ancestor = this.findShapeAncestor( + targetShape, + (ancestor) => ancestor.parentId === siblingShape.parentId + ) + + return ancestor + } + + /** + * Get whether the given shape is the descendant of the given page. + * + * @example + * ```ts + * editor.isShapeInPage(myShape) + * editor.isShapeInPage(myShape, 'page1') + * ``` + * + * @param shape - The shape to check. + * @param pageId - The id of the page to check against. Defaults to the current page. + * + * @public + */ + isShapeInPage(shape: TLShape | TLShapeId, pageId = this.getCurrentPageId()): boolean { + return this.getAncestorPageId(shape) === pageId + } + + /** + * Get the id of the containing page for a given shape. + * + * @param shape - The shape to get the page id for. + * + * @returns The id of the page that contains the shape, or undefined if the shape is undefined. + * + * @public + */ + getAncestorPageId(shape?: TLShape | TLShapeId): TLPageId | undefined { + const id = typeof shape === 'string' ? shape : shape?.id + const _shape = id && this.getShape(id) + if (!_shape) return undefined + if (isPageId(_shape.parentId)) { + return _shape.parentId + } else { + return this.getAncestorPageId(this.getShape(_shape.parentId)) + } + } + + // Parents and children + + /** + * A cache of parents to children. + * + * @internal + */ + private readonly _parentIdsToChildIds: ReturnType + + /** + * Reparent shapes to a new parent. This operation preserves the shape's current page positions / + * rotations. + * + * @example + * ```ts + * editor.reparentShapes([box1, box2], 'frame1') + * editor.reparentShapes([box1.id, box2.id], 'frame1') + * editor.reparentShapes([box1.id, box2.id], 'frame1', 4) + * ``` + * + * @param shapes - The shapes (or shape ids) of the shapes to reparent. + * @param parentId - The id of the new parent shape. + * @param insertIndex - The index to insert the children. + * + * @public + */ + reparentShapes(shapes: TLShapeId[] | TLShape[], parentId: TLParentId, insertIndex?: IndexKey) { + const ids = toShapeIds(shapes) + if (ids.length === 0) return this + + const changes: TLShapePartial[] = [] + + const parentTransform = isPageId(parentId) + ? Mat.Identity() + : this.getShapePageTransform(parentId)! + + const parentPageRotation = parentTransform.rotation() + + let indices: IndexKey[] = [] + + const sibs = compact(this.getSortedChildIdsForParent(parentId).map((id) => this.getShape(id))) + + if (insertIndex) { + const sibWithInsertIndex = sibs.find((s) => s.index === insertIndex) + if (sibWithInsertIndex) { + // If there's a sibling with the same index as the insert index... + const sibAbove = sibs[sibs.indexOf(sibWithInsertIndex) + 1] + if (sibAbove) { + // If the sibling has a sibling above it, insert the shapes + // between the sibling and its sibling above it. + indices = getIndicesBetween(insertIndex, sibAbove.index, ids.length) + } else { + // Or if the sibling is the top sibling, insert the shapes + // above the sibling + indices = getIndicesAbove(insertIndex, ids.length) + } + } else { + // If there's no collision, then we can start at the insert index + const sibAbove = sibs.sort(sortByIndex).find((s) => s.index > insertIndex) + + if (sibAbove) { + // If the siblings include a sibling with a higher index, insert the shapes + // between the insert index and the sibling with the higher index. + indices = getIndicesBetween(insertIndex, sibAbove.index, ids.length) + } else { + // Otherwise, we're at the top of the order, so insert the shapes above + // the insert index. + indices = getIndicesAbove(insertIndex, ids.length) + } + } + } else { + // If insert index is not specified, start the index at the top. + const sib = sibs.length && sibs[sibs.length - 1] + indices = sib ? getIndicesAbove(sib.index, ids.length) : getIndices(ids.length) + } + + const invertedParentTransform = parentTransform.clone().invert() + + const shapesToReparent = compact(ids.map((id) => this.getShape(id))).sort(sortByIndex) + + // Ignore locked shapes so that we can reparent locked shapes, for example + // when a locked shape's parent is deleted. + this.run( + () => { + for (let i = 0; i < shapesToReparent.length; i++) { + const shape = shapesToReparent[i] + + const pageTransform = this.getShapePageTransform(shape) + const newPoint = invertedParentTransform.applyToPoint(pageTransform.point()) + const newRotation = pageTransform.rotation() - parentPageRotation + + if (shape.id === parentId) { + throw Error('Attempted to reparent a shape to itself!') + } + + changes.push({ + id: shape.id, + type: shape.type, + parentId: parentId, + x: newPoint.x, + y: newPoint.y, + rotation: newRotation, + index: indices[i], + }) + } + + this.updateShapes(changes) + }, + { ignoreShapeLock: true } + ) + + return this + } + + /** + * Get the index above the highest child of a given parent. + * + * @param parent - The parent (or the id) of the parent. + * + * @returns The index. + * + * @public + */ + getHighestIndexForParent(parent: TLParentId | TLPage | TLShape): IndexKey { + const parentId = typeof parent === 'string' ? parent : parent.id + const children = this._parentIdsToChildIds.get()[parentId] + + if (!children || children.length === 0) { + return getIndexAbove(ZERO_INDEX_KEY) + } + const shape = this.getShape(children[children.length - 1])! + return getIndexAbove(shape.index) + } + + /** + * Get an array of all the children of a shape. + * + * @example + * ```ts + * editor.getSortedChildIdsForParent('frame1') + * ``` + * + * @param parent - The parent (or the id) of the parent shape. + * + * @public + */ + getSortedChildIdsForParent(parent: TLParentId | TLPage | TLShape): TLShapeId[] { + const parentId = typeof parent === 'string' ? parent : parent.id + const ids = this._parentIdsToChildIds.get()[parentId] + if (!ids) return EMPTY_ARRAY + return ids + } + + /** + * Run a visitor function for all descendants of a shape. + * + * @example + * ```ts + * editor.visitDescendants('frame1', myCallback) + * ``` + * + * @param parent - The parent (or the id) of the parent shape. + * @param visitor - The visitor function. + * + * @public + */ + visitDescendants( + parent: TLParentId | TLPage | TLShape, + visitor: (id: TLShapeId) => void | false + ): this { + const children = this.getSortedChildIdsForParent(parent) + for (const id of children) { + if (visitor(id) === false) continue + this.visitDescendants(id, visitor) + } + return this + } + + /** + * Get the shape ids of all descendants of the given shapes (including the shapes themselves). IDs are returned in z-index order. + * + * @param ids - The ids of the shapes to get descendants of. + * + * @returns The descendant ids. + * + * @public + */ + getShapeAndDescendantIds(ids: TLShapeId[]): Set { + const shapeIds = new Set() + for (const shape of compact(ids.map((id) => this.getShape(id))).sort(sortByIndex)) { + shapeIds.add(shape.id) + this.visitDescendants(shape, (descendantId) => { + shapeIds.add(descendantId) + }) + } + return shapeIds + } + + /** + * Get the shape that some shapes should be dropped on at a given point. + * + * @param point - The point to find the parent for. + * @param droppingShapes - The shapes that are being dropped. + * + * @returns The shape to drop on. + * + * @public + */ + getDraggingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined { + // get fresh moving shapes + const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter( + (s) => !s.isLocked && !this.isShapeHidden(s) + ) + // Descendants of the dragged shapes can't be the target, otherwise dragging a frame out of + // its parent reports a nested child as the target and the parent never sees the drag leave + const excludedIds = this.getShapeAndDescendantIds(draggingShapes.map((s) => s.id)) + + const maybeDraggingOverShapes = this.getShapesAtPoint(point, { + hitInside: true, + margin: 0, + }).filter( + (s) => + !droppingShapes.includes(s) && + !s.isLocked && + !this.isShapeHidden(s) && + !excludedIds.has(s.id) + ) + + for (const maybeDraggingOverShape of maybeDraggingOverShapes) { + const shapeUtil = this.getShapeUtil(maybeDraggingOverShape) + // Any shape that can handle any dragging interactions is a valid target + if ( + shapeUtil.onDragShapesOver || + shapeUtil.onDragShapesIn || + shapeUtil.onDragShapesOut || + shapeUtil.onDropShapesOver + ) { + return maybeDraggingOverShape + } + } + } + + /** + * Get the shape that should be selected when you click on a given shape, assuming there is + * nothing already selected. It will not return anything higher than or including the current + * focus layer. + * + * @param shape - The shape to get the outermost selectable shape for. + * @param filter - A function to filter the selectable shapes. + * + * @returns The outermost selectable shape. + * + * @public + */ + getOutermostSelectableShape( + shape: TLShape | TLShapeId, + filter?: (shape: TLShape) => boolean + ): TLShape { + const id = typeof shape === 'string' ? shape : shape.id + const freshShape = this.getShape(id)! + let match = freshShape + let node = freshShape as TLShape | undefined + + const focusedGroup = this.getFocusedGroup() + + while (node) { + if ( + this.isShapeOfType(node, 'group') && + focusedGroup?.id !== node.id && + !this.hasAncestor(focusedGroup, node.id) && + (filter?.(node) ?? true) + ) { + match = node + } else if (focusedGroup?.id === node.id) { + break + } + node = this.getShapeParent(node) + } + + return match + } + + /* -------------------- Bindings -------------------- */ + + @computed + private _getBindingsIndexCache() { + const index = bindingsIndex(this) + return this.store.createComputedCache( + 'bindingsIndex', + (shape) => { + return index.get().get(shape.id) + }, + // we can ignore the shape equality check here because the index is + // computed incrementally based on what bindings are in the store + { areRecordsEqual: () => true } + ) + } + + /** + * Get a binding from the store by its ID if it exists. + */ + getBinding(id: TLBindingId): TLBinding | undefined { + return this.store.get(id) as TLBinding | undefined + } + + /** + * Get all bindings of a certain type _from_ a particular shape. These are the bindings whose + * `fromId` matched the shape's ID. + */ + getBindingsFromShape( + shape: TLShape | TLShapeId, + type: K + ): Extract[] + getBindingsFromShape( + shape: TLShape | TLShapeId, + type: Binding['type'] + ): Binding[] + getBindingsFromShape( + shape: TLShape | TLShapeId, + type: Binding['type'] + ): Binding[] { + const id = typeof shape === 'string' ? shape : shape.id + return this.getBindingsInvolvingShape(id).filter( + (b) => b.fromId === id && b.type === type + ) as Binding[] + } + + /** + * Get all bindings of a certain type _to_ a particular shape. These are the bindings whose + * `toId` matches the shape's ID. + */ + getBindingsToShape( + shape: TLShape | TLShapeId, + type: K + ): Extract[] + getBindingsToShape( + shape: TLShape | TLShapeId, + type: Binding['type'] + ): Binding[] + getBindingsToShape( + shape: TLShape | TLShapeId, + type: Binding['type'] + ): Binding[] { + const id = typeof shape === 'string' ? shape : shape.id + return this.getBindingsInvolvingShape(id).filter( + (b) => b.toId === id && b.type === type + ) as Binding[] + } + + /** + * Get all bindings involving a particular shape. This includes bindings where the shape is the + * `fromId` or `toId`. If a type is provided, only bindings of that type are returned. + */ + getBindingsInvolvingShape( + shape: TLShape | TLShapeId, + type: K + ): Extract[] + getBindingsInvolvingShape( + shape: TLShape | TLShapeId, + type?: Binding['type'] + ): Binding[] + getBindingsInvolvingShape( + shape: TLShape | TLShapeId, + type?: Binding['type'] + ): Binding[] { + const id = typeof shape === 'string' ? shape : shape.id + const result = this._getBindingsIndexCache().get(id) ?? EMPTY_ARRAY + if (!type) return result as Binding[] + return result.filter((b) => b.type === type) as Binding[] + } + + /** + * Create bindings from a list of partial bindings. You can omit the ID and most props of a + * binding, but the `type`, `toId`, and `fromId` must all be provided. + */ + createBindings(partials: TLBindingCreate[]) { + if (this.getIsReadonly()) return this + + const bindings: TLBinding[] = [] + for (const partial of partials) { + const fromShape = this.getShape(partial.fromId) + const toShape = this.getShape(partial.toId) + if (!fromShape || !toShape) continue + if (!this.canBindShapes({ fromShape, toShape, binding: partial })) continue + + const util = this.getBindingUtil(partial.type) + const defaultProps = util.getDefaultProps() + const binding = this.store.schema.types.binding.create({ + ...partial, + id: partial.id ?? createBindingId(), + props: { + ...defaultProps, + ...partial.props, + }, + }) as TLBinding + + bindings.push(binding) + } + + this.store.put(bindings) + return this + } + + /** + * Create a single binding from a partial. You can omit the ID and most props of a binding, but + * the `type`, `toId`, and `fromId` must all be provided. + */ + createBinding(partial: TLBindingCreate) { + return this.createBindings([partial]) + } + + /** + * Update bindings from a list of partial bindings. Each partial must include an ID, which will + * be used to match the binding to it's existing record. If there is no existing record, that + * binding is skipped. The changes from the partial are merged into the existing record. + */ + updateBindings(partials: (TLBindingUpdate | null | undefined)[]) { + if (this.getIsReadonly()) return this + + const updated: TLBinding[] = [] + + for (const partial of partials) { + if (!partial) continue + + const current = this.getBinding(partial.id) + if (!current) continue + + const updatedBinding = applyPartialToRecordWithProps(current, partial) + if (updatedBinding === current) continue + + const fromShape = this.getShape(updatedBinding.fromId) + const toShape = this.getShape(updatedBinding.toId) + if (!fromShape || !toShape) continue + if (!this.canBindShapes({ fromShape, toShape, binding: updatedBinding })) continue + + updated.push(updatedBinding) + } + + this.store.put(updated) + + return this + } + + /** + * Update a binding from a partial binding. Each partial must include an ID, which will be used + * to match the binding to it's existing record. If there is no existing record, that binding is + * skipped. The changes from the partial are merged into the existing record. + */ + updateBinding(partial: TLBindingUpdate) { + return this.updateBindings([partial]) + } + + /** + * Delete several bindings by their IDs. If a binding ID doesn't exist, it's ignored. + */ + deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) { + if (this.getIsReadonly()) return this + return this._deleteBindings(bindings, { isolateShapes }) + } + + /** + * Unguarded so that withIsolatedShapes can transiently isolate shapes for copy and export in + * readonly mode; the public deleteBindings is what readonly blocks. + * + * @internal + */ + _deleteBindings(bindings: (TLBinding | TLBindingId)[], { isolateShapes = false } = {}) { + const ids = bindings.map((binding) => (typeof binding === 'string' ? binding : binding.id)) + if (isolateShapes) { + this.store.atomic(() => { + for (const id of ids) { + const binding = this.getBinding(id) + if (!binding) continue + const util = this.getBindingUtil(binding) + util.onBeforeIsolateFromShape?.({ binding, removedShape: this.getShape(binding.toId)! }) + util.onBeforeIsolateToShape?.({ binding, removedShape: this.getShape(binding.fromId)! }) + this.store.remove([id]) + } + }) + } else { + this.store.remove(ids) + } + return this + } + /** + * Delete a binding by its ID. If the binding doesn't exist, it's ignored. + */ + deleteBinding(binding: TLBinding | TLBindingId, opts?: Parameters[1]) { + return this.deleteBindings([binding], opts) + } + canBindShapes({ + fromShape, + toShape, + binding, + }: { + fromShape: TLShape | { type: TLShape['type'] } | TLShape['type'] + toShape: TLShape | { type: TLShape['type'] } | TLShape['type'] + binding: TLBinding | { type: TLBinding['type'] } | TLBinding['type'] + }): boolean { + const fromShapeType = typeof fromShape === 'string' ? fromShape : fromShape.type + const toShapeType = typeof toShape === 'string' ? toShape : toShape.type + const bindingType = typeof binding === 'string' ? binding : binding.type + + const canBindOpts: TLShapeUtilCanBindOpts = { + fromShape: typeof fromShape === 'string' ? { type: fromShape } : fromShape, + toShape: typeof toShape === 'string' ? { type: toShape } : toShape, + bindingType, + fromShapeType, + toShapeType, + } + + if (fromShapeType === toShapeType) { + return this.getShapeUtil(fromShapeType).canBind(canBindOpts) + } + + return ( + this.getShapeUtil(fromShapeType).canBind(canBindOpts) && + this.getShapeUtil(toShapeType).canBind(canBindOpts) + ) + } + + /* -------------------- Commands -------------------- */ + + /** + * Rotate shapes by a delta in radians. + * + * @example + * ```ts + * editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI) + * editor.rotateShapesBy(editor.getSelectedShapeIds(), Math.PI / 2) + * ``` + * + * @param shapes - The shapes (or shape ids) of the shapes to move. + * @param delta - The delta in radians to apply to the selection rotation. + * @param opts - The options for the rotation. + */ + rotateShapesBy( + shapes: TLShapeId[] | TLShape[], + delta: number, + opts?: { center?: VecLike } + ): this { + const ids = toShapeIds(shapes) + + if (ids.length <= 0) return this + + const snapshot = getRotationSnapshot({ editor: this, ids }) + if (!snapshot) return this + applyRotationToSnapshotShapes({ + delta, + snapshot, + editor: this, + stage: 'one-off', + centerOverride: opts?.center, + }) + + return this + } + + // Gets a shape partial that includes life cycle changes: on translate start, on translate, on translate end + private getChangesToTranslateShape(initialShape: TLShape, newShapeCoords: VecLike): TLShape { + let workingShape = initialShape + const util = this.getShapeUtil(initialShape) + + const afterTranslateStart = util.onTranslateStart?.(workingShape) + if (afterTranslateStart) { + workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateStart) + } + + workingShape = applyPartialToRecordWithProps(workingShape, { + id: initialShape.id, + type: initialShape.type, + x: newShapeCoords.x, + y: newShapeCoords.y, + }) + + const afterTranslate = util.onTranslate?.(initialShape, workingShape) + if (afterTranslate) { + workingShape = applyPartialToRecordWithProps(workingShape, afterTranslate) + } + + const afterTranslateEnd = util.onTranslateEnd?.(initialShape, workingShape) + if (afterTranslateEnd) { + workingShape = applyPartialToRecordWithProps(workingShape, afterTranslateEnd) + } + + return workingShape + } + + // Counter-rotates the page-space delta into the parent's space; without that a child of a rotated + // frame or group would move along its parent's axes instead of the page's. + // todo: a shape laid out together with its own parent moves twice, once with the parent and once + // on its own; the layout commands should skip shapes whose ancestors are also being laid out + private getChangesToTranslateShapeByPageDelta(shape: TLShape, pageDelta: VecLike): TLShape { + const localDelta = Vec.From(pageDelta).rot(-this.getShapeParentTransform(shape).rotation()) + return this.getChangesToTranslateShape(shape, localDelta.add(shape)) + } + + /** + * Move shapes by a delta. + * + * @example + * ```ts + * editor.nudgeShapes(['box1', 'box2'], { x: 8, y: 8 }) + * ``` + * + * @param shapes - The shapes (or shape ids) to move. + * @param offset - The offset to apply to the shapes. + */ + nudgeShapes(shapes: TLShapeId[] | TLShape[], offset: VecLike): this { + const ids = toShapeIds(shapes) + + if (ids.length <= 0) return this + const changes: TLShapePartial[] = [] + + for (const id of ids) { + const shape = this.getShape(id)! + changes.push(this.getChangesToTranslateShapeByPageDelta(shape, offset)) + } + + this.updateShapes(changes) + + return this + } + + /** + * Duplicate shapes. + * + * @example + * ```ts + * editor.duplicateShapes(['box1', 'box2'], { x: 8, y: 8 }) + * editor.duplicateShapes(editor.getSelectedShapes(), { x: 8, y: 8 }) + * ``` + * + * @param shapes - The shapes (or shape ids) to duplicate. + * @param offset - The offset (in pixels) to apply to the duplicated shapes. + * + * @public + */ + duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this { + this.run(() => { + const _ids = toShapeIds(shapes) + + const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids) + if (ids.length <= 0) return this + + const initialIds = new Set(ids) + const shapeIdSet = this.getShapeAndDescendantIds(ids) + + const orderedShapeIds = [...shapeIdSet].reverse() + const shapeIds = new Map() + for (const shapeId of shapeIdSet) { + shapeIds.set(shapeId, createShapeId()) + } + + const { shapesToCreateWithOriginals, bindingsToCreate } = withIsolatedShapes( + this, + shapeIdSet, + (bindingIdsToMaintain) => { + const bindingsToCreate: TLBinding[] = [] + for (const originalId of bindingIdsToMaintain) { + const originalBinding = this.getBinding(originalId) + if (!originalBinding) continue + + const duplicatedId = createBindingId() + bindingsToCreate.push({ + ...originalBinding, + id: duplicatedId, + fromId: assertExists(shapeIds.get(originalBinding.fromId)), + toId: assertExists(shapeIds.get(originalBinding.toId)), + }) + } + + const shapesToCreateWithOriginals: { shape: TLShape; originalShape: TLShape }[] = [] + for (const originalId of orderedShapeIds) { + const duplicatedId = assertExists(shapeIds.get(originalId)) + const originalShape = this.getShape(originalId) + if (!originalShape) continue + + let ox = 0 + let oy = 0 + + // Only offset the roots of the duplicated tree: a descendant that was also passed in + // follows its duplicated parent, so offsetting it too would move it twice + if ( + offset && + initialIds.has(originalId) && + !shapeIdSet.has(originalShape.parentId as TLShapeId) + ) { + const parentTransform = this.getShapeParentTransform(originalShape) + const vec = new Vec(offset.x, offset.y).rot(-parentTransform!.rotation()) + ox = vec.x + oy = vec.y + } + + shapesToCreateWithOriginals.push({ + shape: { + ...originalShape, + id: duplicatedId, + x: originalShape.x + ox, + y: originalShape.y + oy, + // Use a dummy index for now, it will get updated outside of the `withIsolatedShapes` + index: 'a1' as IndexKey, + parentId: + shapeIds.get(originalShape.parentId as TLShapeId) ?? originalShape.parentId, + }, + originalShape, + }) + } + + return { shapesToCreateWithOriginals, bindingsToCreate } + } + ) + + // We will update the indexes after the `withIsolatedShapes`, since we cannot rely on the indexes + // to be correct inside of it. + shapesToCreateWithOriginals.forEach(({ shape, originalShape }) => { + const parentId = originalShape.parentId + const siblings = this.getSortedChildIdsForParent(parentId) + const currentIndex = siblings.indexOf(originalShape.id) + const siblingAboveId = siblings[currentIndex + 1] + const siblingAbove = siblingAboveId ? this.getShape(siblingAboveId) : undefined + + const index = getIndexBetween(originalShape.index, siblingAbove?.index) + + shape.index = index + }) + const shapesToCreate = shapesToCreateWithOriginals.map(({ shape, originalShape }) => { + // Give the shape util a chance to modify the duplicate, e.g. to re-stamp note + // attribution to the current user so we don't forge the original author's identity. + return this.getShapeUtil(shape).onBeforeDuplicate?.(originalShape, shape) ?? shape + }) + + if (!this.canCreateShapes(shapesToCreate)) { + alertMaxShapes(this) + return + } + + this.createShapes(shapesToCreate) + this.createBindings(bindingsToCreate) + + this.setSelectedShapes( + compact( + ids.map((oldId) => { + const newId = shapeIds.get(oldId) + if (!newId) return null + if (!this.getShape(newId)) return null + return newId + }) + ) + ) + + if (offset !== undefined) { + // If we've offset the duplicated shapes, check to see whether their new bounds is entirely + // contained in the current viewport. If not, then animate the camera to be centered on the + // new shapes. + const selectionPageBounds = this.getSelectionPageBounds() + const viewportPageBounds = this.getViewportPageBounds() + if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) { + this.centerOnPoint(selectionPageBounds.center, { + animation: { duration: this.options.animationMediumMs }, + }) + } + } + }) + + return this + } + + /** + * Move shapes to page. + * + * @example + * ```ts + * editor.moveShapesToPage(['box1', 'box2'], 'page1') + * ``` + * + * @param shapes - The shapes (or shape ids) of the shapes to move. + * @param pageId - The id of the page where the shapes will be moved. + * + * @public + */ + moveShapesToPage(shapes: TLShapeId[] | TLShape[], pageId: TLPageId): this { + const ids = toShapeIds(shapes) + + if (ids.length === 0) return this + if (this.getIsReadonly()) return this + + const currentPageId = this.getCurrentPageId() + + if (pageId === currentPageId) return this + if (!this.store.has(pageId)) return this + + // Basically copy the shapes + const content = this.getContentFromCurrentPage(ids) + + // Just to be sure + if (!content) return this + + // If there is no space on pageId, or if the selected shapes + // would take the new page above the limit, don't move the shapes + if (this.getPageShapeIds(pageId).size + content.shapes.length > this.options.maxShapesPerPage) { + alertMaxShapes(this, pageId) + return this + } + + const fromPageZ = this.getCamera().z + + this.run(() => { + // Delete the shapes on the current page + this.deleteShapes(ids) + + // Move to the next page + this.setCurrentPage(pageId) + + // Put the shape content onto the new page; parents and indices will + // be taken care of by the putContent method; make sure to pop any focus + // layers so that the content will be put onto the page. + this.setFocusedGroup(null) + this.selectNone() + this.putContentOntoCurrentPage(content, { + select: true, + preserveIds: true, + preservePosition: true, + }) + + // Force the new page's camera to be at the same zoom level as the + // "from" page's camera, then center the "to" page's camera on the + // pasted shapes + this.setCamera({ ...this.getCamera(), z: fromPageZ }) + this.centerOnPoint(this.getSelectionRotatedPageBounds()!.center) + }) + + return this + } + + /** + * Toggle the lock state of one or more shapes. If there is a mix of locked and unlocked shapes, all shapes will be locked. + * + * @param shapes - The shapes (or shape ids) to toggle. + * + * @public + */ + toggleLock(shapes: TLShapeId[] | TLShape[]): this { + const ids = toShapeIds(shapes) + + if (this.getIsReadonly() || ids.length === 0) return this + + let allLocked = true + const shapesToToggle: TLShape[] = [] + for (const id of ids) { + const shape = this.getShape(id) + if (shape) { + shapesToToggle.push(shape) + if (!shape.isLocked) allLocked = false + } + } + this.run(() => { + if (allLocked) { + this.updateShapes( + shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false })) + ) + } else { + this.updateShapes( + shapesToToggle.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true })) + ) + this.setSelectedShapes([]) + } + }) + + return this + } + + /** + * Send shapes to the back of the page's object list. + * + * @example + * ```ts + * editor.sendToBack(['id1', 'id2']) + * editor.sendToBack(box1, box2) + * ``` + * + * @param shapes - The shapes (or shape ids) to move. + * + * @public + */ + sendToBack(shapes: TLShapeId[] | TLShape[]): this { + const ids = toShapeIds(shapes) + const changes = getReorderingShapesChanges(this, 'toBack', ids, { + considerAllShapes: true, + }) + if (changes) this.updateShapes(changes) + return this + } + + /** + * Send shapes backward in the page's object list. + * + * @example + * ```ts + * editor.sendBackward(['id1', 'id2']) + * editor.sendBackward([box1, box2]) + * ``` + * + * By default, the operation will only consider overlapping shapes. + * To consider all shapes, pass `{ considerAllShapes: true }` in the options. + * + * @example + * ```ts + * editor.sendBackward(['id1', 'id2'], { considerAllShapes: true }) + * ``` + * + * @param shapes - The shapes (or shape ids) to move. + * @param opts - The options for the backward operation. + * + * @public + */ + sendBackward(shapes: TLShapeId[] | TLShape[], opts: { considerAllShapes?: boolean } = {}): this { + const ids = toShapeIds(shapes) + const changes = getReorderingShapesChanges(this, 'backward', ids, opts) + if (changes) this.updateShapes(changes) + return this + } + + /** + * Bring shapes forward in the page's object list. + * + * @example + * ```ts + * editor.bringForward(['id1', 'id2']) + * editor.bringForward(box1, box2) + * ``` + * + * By default, the operation will only consider overlapping shapes. + * To consider all shapes, pass `{ considerAllShapes: true }` in the options. + * + * @example + * ```ts + * editor.bringForward(['id1', 'id2'], { considerAllShapes: true }) + * ``` + * + * @param shapes - The shapes (or shape ids) to move. + * @param opts - The options for the forward operation. + * + * @public + */ + bringForward(shapes: TLShapeId[] | TLShape[], opts: { considerAllShapes?: boolean } = {}): this { + const ids = toShapeIds(shapes) + const changes = getReorderingShapesChanges(this, 'forward', ids, opts) + if (changes) this.updateShapes(changes) + return this + } + + /** + * Bring shapes to the front of the page's object list. + * + * @example + * ```ts + * editor.bringToFront(['id1', 'id2']) + * editor.bringToFront([box1, box2]) + * ``` + * + * @param shapes - The shapes (or shape ids) to move. + * + * @public + */ + bringToFront(shapes: TLShapeId[] | TLShape[]): this { + const ids = toShapeIds(shapes) + const changes = getReorderingShapesChanges(this, 'toFront', ids) + if (changes) this.updateShapes(changes) + return this + } + + /** + * Shared clustering logic for layout methods. Resolves shapes, optionally filters to + * axis-aligned shapes, checks canBeLaidOut, and groups shapes into clusters via arrow bindings. + * + * @internal + */ + private getShapeClusters( + shapes: TLShapeId[] | TLShape[], + type: TLShapeUtilCanBeLaidOutOpts['type'], + opts?: { filterAxisAligned?: boolean } + ): { clusters: { shapes: TLShape[]; pageBounds: Box }[]; allBounds: Box[] } { + const ids = toShapeIds(shapes) + + // always fresh shapes + let freshShapes = compact(ids.map((id) => this.getShape(id))) + + // optionally filter to axis-aligned shapes (rotation is a multiple of 90 degrees) + if (opts?.filterAxisAligned) { + freshShapes = freshShapes.filter((s) => { + // Page rotations come back through atan2 on a composed matrix, so a 270° shape can land + // a hair either side of the multiple; an exact === 0 check drops it + const remainder = Math.abs(this.getShapePageTransform(s).rotation() % HALF_PI) + return Math.min(remainder, HALF_PI - remainder) < 1e-9 + }) + } + + const clusters: { shapes: TLShape[]; pageBounds: Box }[] = [] + const allBounds: Box[] = [] + const visited = new Set() + + for (const shape of freshShapes) { + if (visited.has(shape.id)) continue + visited.add(shape.id) + + const shapePageBounds = this.getShapePageBounds(shape) + if (!shapePageBounds) continue + + if ( + !this.getShapeUtil(shape).canBeLaidOut?.(shape, { + type, + shapes: freshShapes, + }) + ) { + continue + } + + const shapesMovingTogether = [shape] + const boundsOfShapesMovingTogether: Box[] = [shapePageBounds] + + // Seed with bindings in both directions, otherwise an arrow visited before the shapes it + // binds ends up in a cluster of its own and the result depends on input order + this.collectShapesViaArrowBindings({ + bindings: this.getBindingsInvolvingShape(shape.id, 'arrow'), + initialShapes: freshShapes, + resultShapes: shapesMovingTogether, + resultBounds: boundsOfShapesMovingTogether, + visited, + }) + + const commonPageBounds = Box.Common(boundsOfShapesMovingTogether) + if (!commonPageBounds) continue + + clusters.push({ + shapes: shapesMovingTogether, + pageBounds: commonPageBounds, + }) + + allBounds.push(commonPageBounds) + } + + return { clusters, allBounds } + } + + /** + * @internal + */ + private collectShapesViaArrowBindings(info: { + initialShapes: TLShape[] + resultShapes: TLShape[] + resultBounds: Box[] + bindings: TLBinding[] + visited: Set + }) { + const { initialShapes, resultShapes, resultBounds, bindings, visited } = info + for (const binding of bindings) { + for (const id of [binding.fromId, binding.toId]) { + if (!visited.has(id)) { + const aligningShape = initialShapes.find((s) => s.id === id) + if (aligningShape && !visited.has(aligningShape.id)) { + visited.add(aligningShape.id) + const shapePageBounds = this.getShapePageBounds(aligningShape) + if (!shapePageBounds) continue + resultShapes.push(aligningShape) + resultBounds.push(shapePageBounds) + this.collectShapesViaArrowBindings({ + ...info, + bindings: this.getBindingsInvolvingShape(aligningShape, 'arrow'), + }) + } + } + } + } + } + + /** + * Flip shape positions. + * + * @example + * ```ts + * editor.flipShapes([box1, box2], 'horizontal', 32) + * editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal', 32) + * ``` + * + * @param shapes - The ids of the shapes to flip. + * @param operation - Whether to flip horizontally or vertically. + * + * @public + */ + flipShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this { + if (this.getIsReadonly()) return this + + const ids = toShapeIds(shapes) + + // Collect a greedy list of shapes to flip + const shapesToFlipFirstPass = compact(ids.map((id) => this.getShape(id))) + + for (const shape of shapesToFlipFirstPass) { + if (this.isShapeOfType(shape, 'group')) { + const childrenOfGroups = compact( + this.getSortedChildIdsForParent(shape.id).map((id) => this.getShape(id)) + ) + shapesToFlipFirstPass.push(...childrenOfGroups) + } + } + + // exclude shapes that can't be flipped + const shapesToFlip: { + shape: TLShape + localBounds: Box + pageTransform: Mat + isAspectRatioLocked: boolean + }[] = [] + + const allBounds: Box[] = [] + + for (const shape of shapesToFlipFirstPass) { + const util = this.getShapeUtil(shape) + if ( + !util.canBeLaidOut(shape, { + type: 'flip', + shapes: shapesToFlipFirstPass, + }) + ) { + continue + } + + const pageBounds = this.getShapePageBounds(shape) + const localBounds = this.getShapeGeometry(shape).bounds + const pageTransform = this.getShapePageTransform(shape.id) + if (!(pageBounds && localBounds && pageTransform)) continue + shapesToFlip.push({ + shape, + localBounds, + pageTransform, + isAspectRatioLocked: util.isAspectRatioLocked(shape), + }) + allBounds.push(pageBounds) + } + + if (!shapesToFlip.length) return this + + const scaleOriginPage = Box.Common(allBounds).center + + this.run(() => { + for (const { shape, localBounds, pageTransform, isAspectRatioLocked } of shapesToFlip) { + this.resizeShape( + shape.id, + { x: operation === 'horizontal' ? -1 : 1, y: operation === 'vertical' ? -1 : 1 }, + { + initialBounds: localBounds, + initialPageTransform: pageTransform, + initialShape: shape, + isAspectRatioLocked, + mode: 'scale_shape', + scaleOrigin: scaleOriginPage, + scaleAxisRotation: 0, + } + ) + } + }) + + return this + } + + /** + * Stack shape. + * + * @example + * ```ts + * editor.stackShapes([box1, box2], 'horizontal') + * editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal') + * ``` + * + * @param shapes - The shapes (or shape ids) to stack. + * @param operation - Whether to stack horizontally or vertically. + * @param gap - The gap to leave between shapes. By default, uses the editor's `adjacentShapeMargin` option. + * + * @public + */ + stackShapes( + shapes: TLShapeId[] | TLShape[], + operation: 'horizontal' | 'vertical', + gap?: number + ): this { + const _gap = gap ?? this.options.adjacentShapeMargin + if (this.getIsReadonly()) return this + + // todo: this has a lot of extra code to handle stacking with custom gaps or auto gaps or other things like that. I don't think anyone has ever used this stuff. + + const { clusters: shapeClustersToStack } = this.getShapeClusters(shapes, 'stack') + + const len = shapeClustersToStack.length + if ((_gap === 0 && len < 3) || len < 2) return this + + const { val, min, max, dim } = AXIS[operation] + + let shapeGap: number = 0 + + // Stack in spatial order rather than input (z) order, otherwise shapes swap places + shapeClustersToStack.sort((a, b) => a.pageBounds[min] - b.pageBounds[min]) + + if (_gap === 0) { + // note: this is not used in the current tldraw.com; there we use a specified stack + + const gaps: Record = {} + + // Collect all of the gaps between shapes. We want to find + // patterns (equal gaps between shapes) and use the most common + // one as the gap for all of the shapes. + for (let i = 0; i < len - 1; i++) { + const currCluster = shapeClustersToStack[i] + const nextCluster = shapeClustersToStack[i + 1] + const gap = nextCluster.pageBounds[min] - currCluster.pageBounds[max] + if (!gaps[gap]) { + gaps[gap] = 0 + } + gaps[gap]++ + } + + // Which gap is the most common? + let maxCount = 1 + for (const [gap, count] of Object.entries(gaps)) { + if (count > maxCount) { + maxCount = count + shapeGap = parseFloat(gap) + } + } + + // If there is no most-common gap, use the average gap. + if (maxCount === 1) { + let totalCount = 0 + for (const [gap, count] of Object.entries(gaps)) { + shapeGap += parseFloat(gap) * count + totalCount += count + } + shapeGap /= totalCount + } + } else { + // If a gap was provided, then use that instead. + shapeGap = _gap + } + + const changes: TLShapePartial[] = [] + + let v = shapeClustersToStack[0].pageBounds[max] + + for (let i = 1; i < shapeClustersToStack.length; i++) { + const { shapes, pageBounds } = shapeClustersToStack[i] + const delta = new Vec() + delta[val] = v + shapeGap - pageBounds[val] + + for (const shape of shapes) { + changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta)) + } + + v += pageBounds[dim] + shapeGap + } + + this.updateShapes(changes) + return this + } + + /** + * Pack shapes into a grid centered on their current position. Based on potpack (https://github.com/mapbox/potpack). + * + * @example + * ```ts + * editor.packShapes([box1, box2]) + * editor.packShapes(editor.getSelectedShapeIds(), 32) + * ``` + * + * + * @param shapes - The shapes (or shape ids) to pack. + * @param gap - The padding to apply to the packed shapes. Defaults to the editor's `adjacentShapeMargin` option. + */ + packShapes(shapes: TLShapeId[] | TLShape[], _gap?: number): this { + if (this.getIsReadonly()) return this + + const gap = _gap ?? this.options.adjacentShapeMargin + + const { clusters, allBounds } = this.getShapeClusters(shapes, 'pack') + + const shapeClustersToPack = clusters.map((cluster) => ({ + ...cluster, + nextPageBounds: cluster.pageBounds.clone(), + })) + + if (shapeClustersToPack.length < 2) return this + + let area = 0 + for (const { pageBounds } of shapeClustersToPack) { + area += pageBounds.width * pageBounds.height + } + + const commonBounds = Box.Common(allBounds) + + const maxWidth = commonBounds.width + + // sort the shape clusters by width and then height, descending: potpack fills each row's + // right-hand space with the shapes that follow, so they must be no taller than the row + shapeClustersToPack + .sort((a, b) => b.pageBounds.width - a.pageBounds.width) + .sort((a, b) => b.pageBounds.height - a.pageBounds.height) + + // Start with is (sort of) the square of the area + const startWidth = Math.max(Math.ceil(Math.sqrt(area / 0.95)), maxWidth) + + // first shape fills the width and is infinitely tall + const spaces: Box[] = [new Box(commonBounds.x, commonBounds.y, startWidth, Infinity)] + + let width = 0 + let height = 0 + let space: Box + let last: Box + + for (const { nextPageBounds } of shapeClustersToPack) { + // starting at the back (smaller shapes) + for (let i = spaces.length - 1; i >= 0; i--) { + space = spaces[i] + + // find a space that is big enough to contain the shape + if (nextPageBounds.width > space.width || nextPageBounds.height > space.height) continue + + // add the shape to its top-left corner + nextPageBounds.x = space.x + nextPageBounds.y = space.y + + height = Math.max(height, nextPageBounds.maxY) + width = Math.max(width, nextPageBounds.maxX) + + if (nextPageBounds.width === space.width && nextPageBounds.height === space.height) { + // remove the space on a perfect fit + last = spaces.pop()! + if (i < spaces.length) spaces[i] = last + } else if (nextPageBounds.height === space.height) { + // fit the shape into the space (width) + space.x += nextPageBounds.width + gap + space.width -= nextPageBounds.width + gap + } else if (nextPageBounds.width === space.width) { + // fit the shape into the space (height) + space.y += nextPageBounds.height + gap + space.height -= nextPageBounds.height + gap + } else { + // split the space into two spaces + spaces.push( + new Box( + space.x + (nextPageBounds.width + gap), + space.y, + space.width - (nextPageBounds.width + gap), + nextPageBounds.height + ) + ) + space.y += nextPageBounds.height + gap + space.height -= nextPageBounds.height + gap + } + break + } + } + + const commonAfter = Box.Common(shapeClustersToPack.map((s) => s.nextPageBounds)) + const centerDelta = Vec.Sub(commonBounds.center, commonAfter.center) + + const changes: TLShapePartial[] = [] + + for (const { shapes, pageBounds, nextPageBounds } of shapeClustersToPack) { + const delta = Vec.Sub(nextPageBounds.point, pageBounds.point).add(centerDelta) + + for (const shape of shapes) { + changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta)) + } + } + + if (changes.length) { + this.updateShapes(changes) + } + + return this + } + + /** + * Align shape positions. + * + * @example + * ```ts + * editor.alignShapes([box1, box2], 'left') + * editor.alignShapes(editor.getSelectedShapeIds(), 'left') + * ``` + * + * @param shapes - The shapes (or shape ids) to align. + * @param operation - The align operation to apply. + * + * @public + */ + alignShapes( + shapes: TLShapeId[] | TLShape[], + operation: + | 'left' + | 'center-horizontal' + | 'right' + | 'top' + | 'center-vertical' + | 'bottom' + | 'center' + ): this { + if (this.getIsReadonly()) return this + if (operation === 'center') { + return this.alignShapes(shapes, 'center-horizontal').alignShapes(shapes, 'center-vertical') + } + + const { clusters: shapeClustersToAlign, allBounds } = this.getShapeClusters(shapes, 'align') + + if (shapeClustersToAlign.length < 2) return this + + const commonBounds = Box.Common(allBounds) + + const changes: TLShapePartial[] = [] + + shapeClustersToAlign.forEach(({ shapes, pageBounds }) => { + const delta = new Vec() + + switch (operation) { + case 'top': { + delta.y = commonBounds.minY - pageBounds.minY + break + } + case 'center-vertical': { + delta.y = commonBounds.midY - pageBounds.minY - pageBounds.height / 2 + break + } + case 'bottom': { + delta.y = commonBounds.maxY - pageBounds.minY - pageBounds.height + break + } + case 'left': { + delta.x = commonBounds.minX - pageBounds.minX + break + } + case 'center-horizontal': { + delta.x = commonBounds.midX - pageBounds.minX - pageBounds.width / 2 + break + } + case 'right': { + delta.x = commonBounds.maxX - pageBounds.minX - pageBounds.width + break + } + } + + for (const shape of shapes) { + changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta)) + } + }) + + this.updateShapes(changes) + return this + } + + /** + * Distribute shape positions. + * + * @example + * ```ts + * editor.distributeShapes([box1, box2], 'horizontal') + * editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal') + * ``` + * + * @param shapes - The shapes (or shape ids) to distribute. + * @param operation - Whether to distribute shapes horizontally or vertically. + * + * @public + */ + distributeShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this { + if (this.getIsReadonly()) return this + + const { clusters: shapeClustersToDistribute } = this.getShapeClusters(shapes, 'distribute') + + if (shapeClustersToDistribute.length < 3) return this + + const { val, min, max, dim } = AXIS[operation] + const changes: TLShapePartial[] = [] + + const first = shapeClustersToDistribute.sort((a, b) => a.pageBounds[min] - b.pageBounds[min])[0] + const last = shapeClustersToDistribute.sort((a, b) => b.pageBounds[max] - a.pageBounds[max])[0] + + // If the first shape group is also the last shape group, distribute without it + if (first === last) { + const excludedShapeIds = new Set(first.shapes.map((s) => s.id)) + const ids = toShapeIds(shapes) + return this.distributeShapes( + ids.filter((id) => !excludedShapeIds.has(id)), + operation + ) + } + + const shapeClustersToMove = shapeClustersToDistribute + .filter((shape) => shape !== first && shape !== last) + .sort((a, b) => { + if (a.pageBounds[min] === b.pageBounds[min]) { + return a.shapes[0].id < b.shapes[0].id ? -1 : 1 + } + return a.pageBounds[min] - b.pageBounds[min] + }) + + // The gap is the amount of space "left over" between the first and last shape. This can be a negative number if the shapes are overlapping. + const maxFirst = first.pageBounds[max] + const range = last.pageBounds[min] - maxFirst + const summedShapeDimensions = shapeClustersToMove.reduce((acc, s) => acc + s.pageBounds[dim], 0) + const gap = (range - summedShapeDimensions) / (shapeClustersToMove.length + 1) + + for (let v = maxFirst + gap, i = 0; i < shapeClustersToMove.length; i++) { + const { shapes, pageBounds } = shapeClustersToMove[i] + const delta = new Vec() + delta[val] = v - pageBounds[val] + + // If for some reason the new position would be more than the maximum, we need to adjust the delta + // This will likely throw off some of the other placements but hey, it's better than changing the common bounds + if (v + pageBounds[dim] > last.pageBounds[max] - 1) { + delta[val] = last.pageBounds[max] - pageBounds[max] - 1 + } + + for (const shape of shapes) { + changes.push(this.getChangesToTranslateShapeByPageDelta(shape, delta)) + } + + v += pageBounds[dim] + gap + } + + this.updateShapes(changes) + return this + } + + /** + * Stretch shape sizes and positions to fill their common bounding box. + * + * @example + * ```ts + * editor.stretchShapes([box1, box2], 'horizontal') + * editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') + * ``` + * + * @param shapes - The shapes (or shape ids) to stretch. + * @param operation - Whether to stretch shapes horizontally or vertically. + * + * @public + */ + stretchShapes(shapes: TLShapeId[] | TLShape[], operation: 'horizontal' | 'vertical'): this { + if (this.getIsReadonly()) return this + + const { clusters: shapeClustersToStretch, allBounds } = this.getShapeClusters( + shapes, + 'stretch', + { filterAxisAligned: true } + ) + + if (shapeClustersToStretch.length < 2) return this + + const commonBounds = Box.Common(allBounds) + const { val, min, dim } = AXIS[operation] + + this.run(() => { + shapeClustersToStretch.forEach(({ shapes, pageBounds }) => { + const pageOffset = new Vec() + pageOffset[val] = commonBounds[min] - pageBounds[min] + + const scaleOrigin = pageBounds.center.clone() + scaleOrigin[val] = commonBounds[min] + + const scale = new Vec(1, 1) + scale[val] = commonBounds[dim] / pageBounds[dim] + + for (const shape of shapes) { + // First translate + this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset)) + + // Then resize + this.resizeShape(shape.id, scale, { + initialBounds: this.getShapeGeometry(shape).bounds, + scaleOrigin, + isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape), + scaleAxisRotation: 0, + }) + } + }) + }) + + return this + } + + /** + * Resize and reposition a set of shapes so that their combined page bounds matches the given + * target bounds. + * + * @example + * ```ts + * editor.resizeToBounds([box1, box2], { x: 0, y: 0, w: 500, h: 500 }) + * editor.resizeToBounds(editor.getSelectedShapeIds(), new Box(0, 0, 500, 500)) + * ``` + * + * @param shapes - The shapes (or shape ids) to resize. + * @param bounds - The target bounding box. + * + * @public + */ + resizeToBounds(shapes: TLShapeId[] | TLShape[], bounds: BoxLike): this { + if (this.getIsReadonly()) return this + + const targetBounds = Box.From(bounds) + + const { clusters: shapeClusters, allBounds } = this.getShapeClusters( + shapes, + 'resize_to_bounds', + { filterAxisAligned: true } + ) + + if (shapeClusters.length === 0) return this + + const commonBounds = Box.Common(allBounds) + if (!commonBounds) return this + if (commonBounds.width === 0 || commonBounds.height === 0) return this + + const scaleX = targetBounds.width / commonBounds.width + const scaleY = targetBounds.height / commonBounds.height + const scale = new Vec(scaleX, scaleY) + + shapeClusters.forEach(({ shapes, pageBounds }) => { + const pageOffset = new Vec( + targetBounds.minX - + commonBounds.minX + + (pageBounds.minX - commonBounds.minX) * (scaleX - 1), + targetBounds.minY - commonBounds.minY + (pageBounds.minY - commonBounds.minY) * (scaleY - 1) + ) + + const scaleOrigin = new Vec( + targetBounds.minX + (pageBounds.minX - commonBounds.minX) * scaleX, + targetBounds.minY + (pageBounds.minY - commonBounds.minY) * scaleY + ) + + for (const shape of shapes) { + // First translate + this.updateShape(this.getChangesToTranslateShapeByPageDelta(shape, pageOffset)) + + // Then resize + this.resizeShape(shape.id, scale, { + initialBounds: this.getShapeGeometry(shape).bounds, + scaleOrigin, + isAspectRatioLocked: this.getShapeUtil(shape).isAspectRatioLocked(shape), + scaleAxisRotation: 0, + }) + } + }) + + return this + } + + /** + * Resize a shape. + * + * @param shape - The shape (or the shape id of the shape) to resize. + * @param scale - The scale factor to apply to the shape. + * @param opts - Additional options. + * + * @public + */ + resizeShape(shape: TLShapeId | TLShape, scale: VecLike, opts: TLResizeShapeOptions = {}): this { + const partial = this.getResizeShapePartial(shape, scale, opts) + if (partial) this.updateShapes([partial]) + return this + } + + /** + * Get the update for a resized shape without committing it to the store. Interactions that + * resize many shapes at once use this to collect all of the updates and commit them in a + * single batch. Returns null when there is nothing to update. + * + * Shapes that are rotated out of alignment with the scale axis cannot be resized with a + * single update; those shapes are resized immediately (as `resizeShape` would do) and null + * is returned. + * + * @internal + */ + getResizeShapePartial( + shape: TLShapeId | TLShape, + scale: VecLike, + opts: TLResizeShapeOptions = {} + ): TLShapePartial | null { + const id = typeof shape === 'string' ? shape : shape.id + if (this.getIsReadonly()) return null + + if (!Number.isFinite(scale.x)) scale = new Vec(1, scale.y) + if (!Number.isFinite(scale.y)) scale = new Vec(scale.x, 1) + + const initialShape = opts.initialShape ?? this.getShape(id) + if (!initialShape) return null + + const scaleOrigin = opts.scaleOrigin ?? this.getShapePageBounds(id)?.center + if (!scaleOrigin) return null + + const pageTransform = opts.initialPageTransform + ? Mat.Cast(opts.initialPageTransform) + : this.getShapePageTransform(id) + + const pageRotation = pageTransform.rotation() + + const scaleAxisRotation = opts.scaleAxisRotation ?? pageRotation + + const initialBounds = opts.initialBounds ?? this.getShapeGeometry(id).bounds + + if (!initialBounds) return null + + const isAspectRatioLocked = + opts.isAspectRatioLocked ?? this.getShapeUtil(initialShape).isAspectRatioLocked(initialShape) + + if (!areAnglesCompatible(pageRotation, scaleAxisRotation)) { + // shape is awkwardly rotated, keep the aspect ratio locked and adopt the scale factor + // from whichever axis is being scaled the least, to avoid the shape getting bigger + // than the bounds of the selection + // const minScale = Math.min(Math.abs(scale.x), Math.abs(scale.y)) + this._resizeUnalignedShape(id, scale, { + ...opts, + initialBounds, + scaleOrigin, + scaleAxisRotation, + initialPageTransform: pageTransform, + isAspectRatioLocked, + initialShape, + }) + return null + } + + const util = this.getShapeUtil(initialShape) + + if (isAspectRatioLocked) { + if (Math.abs(scale.x) > Math.abs(scale.y)) { + scale = new Vec(scale.x, Math.sign(scale.y) * Math.abs(scale.x)) + } else { + scale = new Vec(Math.sign(scale.x) * Math.abs(scale.y), scale.y) + } + } + + let workingShape: TLShape | null = null + + if (util.onResize && util.canResize(initialShape)) { + // get the model changes from the shape util + const newPagePoint = this._scalePagePoint( + Mat.applyToPoint(pageTransform, new Vec(0, 0)), + scaleOrigin, + scale, + scaleAxisRotation + ) + + const newLocalPoint = this.getPointInParentSpace(initialShape.id, newPagePoint) + + // resize the shape's local bounding box + const myScale = new Vec(scale.x, scale.y) + // the shape is aligned with the rest of the shapes in the selection, but may be + // 90deg offset from the main rotation of the selection, in which case + // we need to flip the width and height scale factors + const areWidthAndHeightAlignedWithCorrectAxis = approximately( + (pageRotation - scaleAxisRotation) % Math.PI, + 0 + ) + myScale.x = areWidthAndHeightAlignedWithCorrectAxis ? scale.x : scale.y + myScale.y = areWidthAndHeightAlignedWithCorrectAxis ? scale.y : scale.x + + // adjust initial model for situations where the parent has moved during the resize + // e.g. groups + const initialPagePoint = Mat.applyToPoint(pageTransform, new Vec()) + + // need to adjust the shape's x and y points in case the parent has moved since start of resizing + const { x, y } = this.getPointInParentSpace(initialShape.id, initialPagePoint) + + workingShape = initialShape + if (!opts.skipStartAndEndCallbacks) { + workingShape = applyPartialToRecordWithProps( + initialShape, + util.onResizeStart?.(initialShape) ?? undefined + ) + } + + const resizedShape = util.onResize( + { ...initialShape, x, y }, + { + newPoint: newLocalPoint, + handle: opts.dragHandle ?? 'bottom_right', + // don't set isSingle to true for children + mode: opts.mode ?? 'scale_shape', + scaleX: myScale.x, + scaleY: myScale.y, + initialBounds, + initialShape, + } + ) + + workingShape = applyPartialToRecordWithProps(workingShape, { + id, + type: initialShape.type as any, + x: newLocalPoint.x, + y: newLocalPoint.y, + ...resizedShape, + }) + + if (!opts.skipStartAndEndCallbacks) { + workingShape = applyPartialToRecordWithProps( + workingShape, + util.onResizeEnd?.(initialShape, workingShape) ?? undefined + ) + } + + if (resizedShape) { + return workingShape + } + } + + // the shape was not resized by its util, so reposition it (rather than resizing it) + // based on where its resized center would be + + const initialPageCenter = Mat.applyToPoint(pageTransform, initialBounds.center) + // get the model changes from the shape util + const newPageCenter = this._scalePagePoint( + initialPageCenter, + scaleOrigin, + scale, + scaleAxisRotation + ) + + const initialPageCenterInParentSpace = this.getPointInParentSpace( + initialShape.id, + initialPageCenter + ) + const newPageCenterInParentSpace = this.getPointInParentSpace(initialShape.id, newPageCenter) + + const delta = Vec.Sub(newPageCenterInParentSpace, initialPageCenterInParentSpace) + + if (workingShape) { + // the util's onResize ran but returned no change; keep the working update (which may + // include changes from onResizeStart / onResizeEnd) and reposition the shape + return { + ...workingShape, + x: initialShape.x + delta.x, + y: initialShape.y + delta.y, + } + } + + return { + id, + type: initialShape.type as any, + x: initialShape.x + delta.x, + y: initialShape.y + delta.y, + } + } + + /** @internal */ + private _scalePagePoint( + point: VecLike, + scaleOrigin: VecLike, + scale: VecLike, + scaleAxisRotation: number + ) { + const relativePoint = Vec.RotWith(point, scaleOrigin, -scaleAxisRotation).sub(scaleOrigin) + + // calculate the new point position relative to the scale origin + const newRelativePagePoint = Vec.MulV(relativePoint, scale) + + // and rotate it back to page coords to get the new page point of the resized shape + const destination = Vec.Add(newRelativePagePoint, scaleOrigin).rotWith( + scaleOrigin, + scaleAxisRotation + ) + + return destination + } + + /** @internal */ + private _resizeUnalignedShape( + id: TLShapeId, + scale: VecLike, + options: { + initialBounds: Box + scaleOrigin: VecLike + scaleAxisRotation: number + initialShape: TLShape + isAspectRatioLocked: boolean + initialPageTransform: MatLike + } + ) { + const { type } = options.initialShape + // If a shape is not aligned with the scale axis we need to treat it differently to avoid skewing. + // Instead of skewing we normalize the scale aspect ratio (i.e. keep the same scale magnitude in both axes) + // and then after applying the scale to the shape we also rotate it if required and translate it so that it's center + // point ends up in the right place. + + const shapeScale = new Vec(scale.x, scale.y) + + // // make sure we are constraining aspect ratio, and using the smallest scale axis to avoid shapes getting bigger + // // than the selection bounding box + if (Math.abs(scale.x) > Math.abs(scale.y)) { + shapeScale.x = Math.sign(scale.x) * Math.abs(scale.y) + } else { + shapeScale.y = Math.sign(scale.y) * Math.abs(scale.x) + } + + // first we can scale the shape about its center point + this.resizeShape(id, shapeScale, { + initialShape: options.initialShape, + initialBounds: options.initialBounds, + isAspectRatioLocked: options.isAspectRatioLocked, + initialPageTransform: options.initialPageTransform, + }) + + // then if the shape is flipped in one axis only, we need to apply an extra rotation + // to make sure the shape is mirrored correctly + if (Math.sign(scale.x) * Math.sign(scale.y) < 0) { + // We need to compute the new local rotation that will result in the negated page rotation. + // For a shape with local rotation `localRot` and parent page rotation `parentRot`: + // - pageRot = parentRot + localRot + // - newPageRot = -pageRot (we want to negate the page rotation) + // - newPageRot = parentRot + newLocalRot (parent hasn't changed) + // - Therefore: newLocalRot = -pageRot - parentRot = -(parentRot + localRot) - parentRot = -localRot - 2*parentRot + const parentRotation = this.getShapeParentTransform(id).rotation() + const rotation = -options.initialShape.rotation - 2 * parentRotation + this.updateShapes([{ id, type, rotation }]) + } + + // Next we need to translate the shape so that it's center point ends up in the right place. + // To do that we first need to calculate the center point of the shape in the current page space before the scale was applied. + const preScaleShapePageCenter = Mat.applyToPoint( + options.initialPageTransform, + options.initialBounds.center + ) + + // And now we scale the center point by the original scale factor + const postScaleShapePageCenter = this._scalePagePoint( + preScaleShapePageCenter, + options.scaleOrigin, + scale, + options.scaleAxisRotation + ) + + // now calculate how far away the shape is from where it needs to be + const pageTransform = this.getShapePageTransform(id) + // We need to use the local bounds center transformed to page space, not the axis-aligned + // page bounds center. This is because the page bounds are axis-aligned and their center + // changes when the rotation changes, but we want to use the same reference point as + // preScaleShapePageCenter (which used initialBounds.center transformed by the page transform). + const currentLocalBounds = this.getShapeGeometry(id).bounds + const currentPageCenter = Mat.applyToPoint(pageTransform, currentLocalBounds.center) + const shapePageTransformOrigin = pageTransform.point() + const pageDelta = Vec.Sub(postScaleShapePageCenter, currentPageCenter) + + // and finally figure out what the shape's new position should be + const postScaleShapePagePoint = Vec.Add(shapePageTransformOrigin, pageDelta) + const { x, y } = this.getPointInParentSpace(id, postScaleShapePagePoint) + + this.updateShapes([{ id, type, x, y }]) + + return this + } + + /** + * Get the initial meta value for a shape. + * + * @example + * ```ts + * editor.getInitialMetaForShape = (shape) => { + * if (shape.type === 'note') { + * return { createdBy: myCurrentUser.id } + * } + * } + * ``` + * + * @param shape - The shape to get the initial meta for. + * + * @public + */ + getInitialMetaForShape(_shape: TLShape): JsonObject { + return {} + } + + /** + * Get whether the provided shape can be created. + * + * @param shape - The shape or shape IDs to check. + * + * @public + */ + canCreateShape(shape: OptionalKeys, 'id'> | TLShape['id']): boolean { + return this.canCreateShapes([shape]) + } + + /** + * Get whether the provided shapes can be created. + * + * @param shapes - The shapes or shape IDs to create. + * + * @public + */ + canCreateShapes( + shapes: (TLShape['id'] | OptionalKeys, 'id'>)[] + ): boolean { + return shapes.length + this.getCurrentPageShapeIds().size <= this.options.maxShapesPerPage + } + + /** + * Create a single shape. + * + * @example + * ```ts + * editor.createShape(myShape) + * editor.createShape({ id: 'box1', type: 'text', props: { richText: toRichText("ok") } }) + * ``` + * + * @param shape - The shape (or shape partial) to create. + * + * @public + */ + createShape(shape: TLCreateShapePartial): this { + this.createShapes([shape]) + return this + } + + /** + * Create shapes. + * + * @example + * ```ts + * editor.createShapes([myShape]) + * editor.createShapes([{ id: 'box1', type: 'text', props: { richText: toRichText("ok") } }]) + * ``` + * + * @param shapes - The shapes (or shape partials) to create. + * + * @public + */ + createShapes(shapes: TLCreateShapePartial[]): this { + if (!Array.isArray(shapes)) { + throw Error('Editor.createShapes: must provide an array of shapes or shape partials') + } + if (this.getIsReadonly()) return this + if (shapes.length <= 0) return this + + const currentPageShapeIds = this.getCurrentPageShapeIds() + + const maxShapesReached = + shapes.length + currentPageShapeIds.size > this.options.maxShapesPerPage + + if (maxShapesReached) { + // can't create more shapes than fit on the page + alertMaxShapes(this) + // todo: throw an error here? Otherwise we'll need to check every time whether the shapes were actually created + return this + } + + const focusedGroupId = this.getFocusedGroupId() + + this.run(() => { + // 1. Parents + + // Make sure that each partial will become the child of either the + // page or another shape that exists (or that will exist) in this page. + + // find last parent id + const currentPageShapesSorted = this.getCurrentPageShapesSorted() + + const partials = shapes.map((partial) => { + if (!partial.id) { + partial = { id: createShapeId(), ...partial } + } + + // If the partial does not provide the parentId OR if the provided + // parentId is NOT in the store AND NOT among the other shapes being + // created, then we need to find a parent for the shape. This can be + // another shape that exists under that point and which can receive + // children of the creating shape's type, or else the page itself. + if ( + !partial.parentId || + !(this.store.has(partial.parentId) || shapes.some((p) => p.id === partial.parentId)) + ) { + let parentId: TLParentId = this.getFocusedGroupId() + + const isPositioned = partial.x !== undefined && partial.y !== undefined + + // If the shape has been explicitly positioned, we'll try to find a parent at + // that position. If not, we'll assume the user isn't deliberately placing the + // shape and the positioning will be handled later by another system. + if (isPositioned) { + for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) { + const parent = currentPageShapesSorted[i] + const util = this.getShapeUtil(parent) + if ( + util.canReceiveNewChildrenOfType(parent, partial.type) && + !this.isShapeHidden(parent) && + this.isPointInShape( + parent, + // If no parent is provided, then we can treat the + // shape's provided x/y as being in the page's space. + { x: partial.x ?? 0, y: partial.y ?? 0 }, + { + margin: 0, + hitInside: true, + } + ) + ) { + parentId = parent.id + break + } + } + } + + const prevParentId = partial.parentId + + // a shape cannot be its own parent. This was a rare issue with frames/groups in the syncFuzz tests. + if (parentId === partial.id) { + parentId = focusedGroupId + } + + // If the parentid has changed... + if (parentId !== prevParentId) { + partial = { ...partial } + + partial.parentId = parentId + + // If the parent is a shape (rather than a page) then insert the + // shapes into the shape's children. Adjust the point and page rotation to be + // preserved relative to the parent. + if (isShapeId(parentId)) { + const point = this.getPointInShapeSpace(this.getShape(parentId)!, { + x: partial.x ?? 0, + y: partial.y ?? 0, + }) + partial.x = point.x + partial.y = point.y + partial.rotation = + -this.getShapePageTransform(parentId)!.rotation() + (partial.rotation ?? 0) + } + } + } + + return partial + }) + + // 2. Indices + + // Get the highest index among the parents of each of the + // the shapes being created; we'll increment from there. + + const parentIndices = new Map() + + const shapeRecordsToCreate: TLShape[] = [] + + const { opacityForNextShape } = this.getInstanceState() + + for (const partial of partials) { + const util = this.getShapeUtil(partial as TLShapePartial) + + // If an index is not explicitly provided, then add the + // shapes to the top of their parents' children; using the + // value in parentsMappedToIndex, get the index above, use it, + // and set it back to parentsMappedToIndex for next time. + let index = partial.index + + if (!index) { + // Hello bug-seeker: have you just created a frame and then a shape + // and found that the shape is automatically the child of the frame? + // this is the reason why! It would be harder to have each shape specify + // the frame as the parent when creating a shape inside of a frame, so + // we do it here. + const parentId = partial.parentId ?? focusedGroupId + + if (!parentIndices.has(parentId)) { + parentIndices.set(parentId, this.getHighestIndexForParent(parentId)) + } + index = parentIndices.get(parentId)! + parentIndices.set(parentId, getIndexAbove(index)) + } + + // The initial props starts as the shape utility's default props + const initialProps = util.getDefaultProps() + + // We then look up each key in the tab state's styles; and if it's there, + // we use the value from the tab state's styles instead of the default. + for (const [style, propKey] of this.styleProps[partial.type]) { + ;(initialProps as any)[propKey] = this.getStyleForNextShape(style) + } + + // When we create the shape, take in the partial (the props coming into the + // function) and merge it with the default props. + let shapeRecordToCreate = ( + this.store.schema.types.shape as RecordType< + TLShape, + 'type' | 'props' | 'index' | 'parentId' + > + ).create({ + ...partial, + index, + opacity: partial.opacity ?? opacityForNextShape, + parentId: partial.parentId ?? focusedGroupId, + props: 'props' in partial ? { ...initialProps, ...partial.props } : initialProps, + }) + + if (shapeRecordToCreate.index === undefined) { + throw Error('no index!') + } + + const next = this.getShapeUtil(shapeRecordToCreate).onBeforeCreate?.(shapeRecordToCreate) + + if (next) { + shapeRecordToCreate = next + } + + shapeRecordsToCreate.push(shapeRecordToCreate) + } + + // Add meta properties, if any, to the shapes + shapeRecordsToCreate.forEach((shape) => { + shape.meta = { + ...this.getInitialMetaForShape(shape), + ...shape.meta, + } + }) + + this.emit('created-shapes', shapeRecordsToCreate) + this.emit('edit') + this.store.put(shapeRecordsToCreate) + }) + + return this + } + + private animatingShapes = new Map() + + /** + * Animate a shape. + * + * @example + * ```ts + * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }) + * editor.animateShape({ id: 'box1', type: 'box', x: 100, y: 100 }, { animation: { duration: 100, ease: t => t*t } }) + * ``` + * + * @param partial - The shape partial to update. + * @param opts - The animation's options. + * + * @public + */ + animateShape( + partial: TLShapePartial | null | undefined, + opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions + ): this { + return this.animateShapes([partial], opts) + } + + /** + * Animate shapes. + * + * @example + * ```ts + * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }]) + * editor.animateShapes([{ id: 'box1', type: 'box', x: 100, y: 100 }], { animation: { duration: 100, ease: t => t*t } }) + * ``` + * + * @param partials - The shape partials to update. + * @param opts - The animation's options. + * + * @public + */ + animateShapes( + partials: (TLShapePartial | null | undefined)[], + opts = { animation: DEFAULT_ANIMATION_OPTIONS } as TLCameraMoveOptions + ): this { + if (!opts.animation) return this + const { duration = 500, easing = EASINGS.linear } = opts.animation + + const animationId = uniqueId() + + let remaining = duration + let t: number + + interface ShapeAnimation { + start: TLShape + end: TLShape + } + + const animations: ShapeAnimation[] = [] + + // Snapshot the lock override now: when this animation is started inside + // editor.run(..., { ignoreShapeLock: true }), run() restores the flag before any tick + // fires, so the final updateShapes below would refuse the locked shape and strand it + const ignoreShapeLock = this._shouldIgnoreShapeLock + + let partial: TLShapePartial | null | undefined, result: ShapeAnimation + for (let i = 0, n = partials.length; i < n; i++) { + partial = partials[i] + if (!partial) continue + + const shape = this.getShape(partial.id)! + if (!shape) continue + + // Apply the same lock rule as updateShapes up front: the intermediate frames go through + // _updateShapes, which doesn't check locks, so a locked shape would otherwise be moved by + // every frame but the last and end up stranded at the penultimate one + const unlocks = shape.isLocked && Object.hasOwn(partial, 'isLocked') && !partial.isLocked + if (!ignoreShapeLock && !unlocks && this.isShapeOrAncestorLocked(shape)) { + continue + } + + result = { + start: structuredClone(shape), + end: applyPartialToRecordWithProps(structuredClone(shape), partial), + } + + animations.push(result) + this.animatingShapes.set(shape.id, animationId) + } + + const handleTick = (elapsed: number) => { + remaining -= elapsed + + if (remaining < 0) { + const { animatingShapes } = this + const partialsToUpdate = partials.filter( + (p) => p && animatingShapes.get(p.id) === animationId + ) + if (partialsToUpdate.length) { + // the regular update shapes also removes the shape from + // the animating shapes set + this.run(() => this.updateShapes(partialsToUpdate), { ignoreShapeLock }) + } + + this.off('tick', handleTick) + return + } + + t = easing(1 - remaining / duration) + + const { animatingShapes } = this + + const updates: TLShapePartial[] = [] + + let animationIdForShape: string | undefined + for (let i = 0, n = animations.length; i < n; i++) { + const { start, end } = animations[i] + // Is the animation for this shape still active? + animationIdForShape = animatingShapes.get(start.id) + if (animationIdForShape !== animationId) continue + + updates.push({ + ...end, + x: lerp(start.x, end.x, t), + y: lerp(start.y, end.y, t), + opacity: lerp(start.opacity, end.opacity, t), + rotation: lerp(start.rotation, end.rotation, t), + props: this.getShapeUtil(end).getInterpolatedProps?.(start, end, t) ?? end.props, + }) + } + + // The _updateShapes method does NOT remove the + // shapes from the animated shapes set + this._updateShapes(updates) + } + + this.on('tick', handleTick) + + return this + } + + /** + * Create a group containing the provided shapes. + * + * @example + * ```ts + * editor.groupShapes([myShape, myOtherShape]) + * editor.groupShapes([myShape, myOtherShape], { groupId: myGroupId, select: false }) + * ``` + * + * @param shapes - The shapes (or shape ids) to group. Defaults to the selected shapes. + * @param opts - An options object. + * + * @public + */ + groupShapes(shapes: TLShape[], opts?: Partial<{ groupId: TLShapeId; select: boolean }>): this + groupShapes(ids: TLShapeId[], opts?: Partial<{ groupId: TLShapeId; select: boolean }>): this + groupShapes( + shapes: TLShapeId[] | TLShape[], + opts = {} as Partial<{ groupId: TLShapeId; select: boolean }> + ): this { + const { groupId = createShapeId(), select = true } = opts + + if (!Array.isArray(shapes)) { + throw Error('Editor.groupShapes: must provide an array of shapes or shape ids') + } + if (this.getIsReadonly()) return this + + const ids = toShapeIds(shapes) + + const shapesToGroup = compact( + (this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) => + this.getShape(id) + ) + ) + // Re-check after the lock filter: Box.Common of nothing is not a valid box and would throw + if (shapesToGroup.length <= 1) return this + + const sortedShapeIds = shapesToGroup.sort(sortByIndex).map((s) => s.id) + const childBounds = compact(shapesToGroup.map((shape) => this.getShapePageBounds(shape))) + const pageBounds = Box.Common(childBounds) + + if (!pageBounds.isValid()) { + throw Error(`Editor.groupShapes: group bounds are invalid (NaN).`) + } + + const { x, y } = pageBounds.point + + const parentId = this.findCommonAncestor(shapesToGroup) ?? this.getCurrentPageId() + + // createShapes bails out when the page is full, so check first; otherwise the shapes get + // reparented into a group that was never created and vanish from the page + if (!this.canCreateShapes([groupId])) { + alertMaxShapes(this) + return this + } + + // If the select tool is mid-interaction, cancel it (get back to idle) before grouping + if (this.isIn('select') && !this.isIn('select.idle')) { + this.cancel() + } + + // Find all the shapes that have the same parentId, and use the highest index. + const shapesWithRootParent = shapesToGroup + .filter((shape) => shape.parentId === parentId) + .sort(sortByIndex) + + const highestIndex = shapesWithRootParent[shapesWithRootParent.length - 1]?.index + + this.run(() => { + this.createShapes([ + { + id: groupId, + type: 'group', + parentId, + index: highestIndex, + x, + y, + opacity: 1, + props: {}, + }, + ]) + this.reparentShapes(sortedShapeIds, groupId) + if (select) { + // the select option determines whether the grouped shapes' children are selected + this.select(groupId) + } + }) + + return this + } + + /** + * Ungroup some shapes. + * + * @example + * ```ts + * editor.ungroupShapes([myGroup, myOtherGroup]) + * editor.ungroupShapes([myGroup], { select: false }) + * ``` + * + * @param shapes - The group shapes (or shape ids) to ungroup. + * @param opts - An options object. + * + * @public + */ + ungroupShapes(ids: TLShapeId[], opts?: Partial<{ select: boolean }>): this + ungroupShapes(shapes: TLShape[], opts?: Partial<{ select: boolean }>): this + ungroupShapes(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<{ select: boolean }>) { + if (this.getIsReadonly()) return this + + const { select = true } = opts + const ids = toShapeIds(shapes) + + const shapesToUngroup = compact( + (this._shouldIgnoreShapeLock ? ids : this._getUnlockedShapeIds(ids)).map((id) => + this.getShape(id) + ) + ) + + if (shapesToUngroup.length === 0) return this + + // If the select tool is mid-interaction, cancel it (get back to idle) before ungrouping + if (this.isIn('select') && !this.isIn('select.idle')) { + this.cancel() + } + + // The ids of the selected shapes after ungrouping; + // these include all of the grouped shapes children, + // plus any shapes that were selected apart from the groups. + const idsToSelect = new Set() + + // Get all groups in the selection + const groups: TLGroupShape[] = [] + + shapesToUngroup.forEach((shape) => { + if (this.isShapeOfType(shape, 'group')) { + groups.push(shape) + } else { + idsToSelect.add(shape.id) + } + }) + + if (groups.length === 0) return this + + this.run(() => { + for (let i = 0, n = groups.length; i < n; i++) { + // Re-read the group: ungrouping an outer group earlier in this loop reparents the + // inner ones, and the stale parentId would send their children into a group that's + // about to be deleted + const group = this.getShape(groups[i].id) + if (!group) continue + const childIds = this.getSortedChildIdsForParent(group.id) + + for (let j = 0, n = childIds.length; j < n; j++) { + idsToSelect.add(childIds[j]) + } + + this.reparentShapes(childIds, group.parentId, group.index) + } + + this.deleteShapes(groups.map((group) => group.id)) + + if (select) { + // the select option determines whether the ungrouped shapes' children are selected + this.select(...idsToSelect) + } + }) + + return this + } + + /** + * Update a shape using a partial of the shape. + * + * @example + * ```ts + * editor.updateShape({ id: 'box1', type: 'geo', props: { w: 100, h: 100 } }) + * ``` + * + * @param partial - The shape partial to update. + * + * @public + */ + updateShape(partial: TLShapePartial | null | undefined) { + this.updateShapes([partial]) + return this + } + + /** + * Update shapes using partials of each shape. + * + * @example + * ```ts + * editor.updateShapes([{ id: 'box1', type: 'geo', props: { w: 100, h: 100 } }]) + * ``` + * + * @param partials - The shape partials to update. + * + * @public + */ + updateShapes(partials: (TLShapePartial | null | undefined)[]) { + const compactedPartials: TLShapePartial[] = [] + + for (let i = 0, n = partials.length; i < n; i++) { + const partial = partials[i] + if (!partial) continue + // Get the current shape referenced by the partial + const shape = this.getShape(partial.id) + if (!shape) continue + + // If we're "forcing" the update, then we'll update the shape + // regardless of whether it / its ancestor is locked + if (!this._shouldIgnoreShapeLock) { + if (shape.isLocked) { + // If the shape itself is locked (even if one of its ancestors is + // also locked) then only allow an update that unlocks the shape. + if (!(Object.hasOwn(partial, 'isLocked') && !partial.isLocked)) { + continue + } + } else if (this.isShapeOrAncestorLocked(shape)) { + // If the shape itself is unlocked, and any of the shape's + // ancestors are locked then we'll skip the update + continue + } + } + + // Remove any animating shapes from the list of partials + this.animatingShapes.delete(partial.id) + + compactedPartials.push(partial) + } + + this._updateShapes(compactedPartials) + return this + } + + /** @internal */ + _updateShapes(_partials: (TLShapePartial | null | undefined)[]) { + if (this.getIsReadonly()) return + + this.run(() => { + const updates = [] + + let shape: TLShape | undefined + let updated: TLShape + + for (let i = 0, n = _partials.length; i < n; i++) { + const partial = _partials[i] + // Skip nullish partials (sometimes created by map fns returning undefined) + if (!partial) continue + + // Get the current shape referenced by the partial + // If there is no current shape, we'll skip this update + shape = this.getShape(partial.id) + if (!shape) continue + + // Get the updated version of the shape + // If the update had no effect, we'll skip this update + updated = applyPartialToRecordWithProps(shape, partial) + if (updated === shape) continue + + //if any shape has an onBeforeUpdate handler, call it and, if the handler returns a + // new shape, replace the old shape with the new one. This is used for example when + // repositioning a text shape based on its new text content. + updated = this.getShapeUtil(shape).onBeforeUpdate?.(shape, updated) ?? updated + + updates.push(updated) + } + + this.emit('edited-shapes', updates) + this.emit('edit') + this.store.put(updates) + }) + } + + /** @internal */ + private _getUnlockedShapeIds(ids: TLShapeId[]): TLShapeId[] { + // Match updateShapes, which also refuses shapes under a locked ancestor; otherwise a child + // of a locked frame can't be moved but can still be deleted or duplicated + return ids.filter((id) => !this.isShapeOrAncestorLocked(id)) + } + + /** + * Delete shapes. + * + * @example + * ```ts + * editor.deleteShapes(['box1', 'box2']) + * ``` + * + * @param ids - The ids of the shapes to delete. + * + * @public + */ + deleteShapes(ids: TLShapeId[]): this + deleteShapes(shapes: TLShape[]): this + deleteShapes(_ids: TLShapeId[] | TLShape[]): this { + if (this.getIsReadonly()) return this + + if (!Array.isArray(_ids)) { + throw Error('Editor.deleteShapes: must provide an array of shapes or shapeIds') + } + + const shapeIds = toShapeIds(_ids) + + // Normally we don't want to delete locked shapes, but if the force option is set, we'll delete them anyway + const shapeIdsToDelete = this._shouldIgnoreShapeLock + ? shapeIds + : this._getUnlockedShapeIds(shapeIds) + + if (shapeIdsToDelete.length === 0) return this + + // We also need to delete these shapes' descendants + const allShapeIdsToDelete = new Set(shapeIdsToDelete) + + for (const id of shapeIdsToDelete) { + this.visitDescendants(id, (childId) => { + allShapeIdsToDelete.add(childId) + }) + } + + this.emit('deleted-shapes', [...allShapeIdsToDelete]) + this.emit('edit') + return this.run(() => this.store.remove([...allShapeIdsToDelete])) + } + + /** + * Delete a shape. + * + * @example + * ```ts + * editor.deleteShape(shape.id) + * ``` + * + * @param id - The id of the shape to delete. + * + * @public + */ + deleteShape(id: TLShapeId): this + deleteShape(shape: TLShape): this + deleteShape(_id: TLShapeId | TLShape) { + this.deleteShapes([typeof _id === 'string' ? _id : _id.id]) + return this + } + + /* --------------------- Styles --------------------- */ + + /** + * Groups have no styles of their own: a style read or write on a selection applies to the + * non-group shapes beneath each group, however deeply nested. Returns those shapes. + * + * @internal + */ + private _getStyleableShapes(shapes: TLShape[]): TLShape[] { + const result: TLShape[] = [] + const visit = (shape: TLShape) => { + if (this.isShapeOfType(shape, 'group')) { + for (const childId of this.getSortedChildIdsForParent(shape.id)) { + const child = this.getShape(childId) + if (child) visit(child) + } + } else { + result.push(shape) + } + } + for (const shape of shapes) visit(shape) + return result + } + + /** + * A derived map containing all current styles among the user's selected shapes. + * + * @internal + */ + @computed + private _getSelectionSharedStyles(): ReadonlySharedStyleMap { + const sharedStyles = new SharedStyleMap() + for (const shape of this._getStyleableShapes(this.getSelectedShapes())) { + for (const [style, propKey] of this.styleProps[shape.type]) { + sharedStyles.applyValue(style, getOwnProperty(shape.props, propKey)) + } + } + + return sharedStyles + } + + /** + * Get the style for the next shape. + * + * @example + * ```ts + * const color = editor.getStyleForNextShape(DefaultColorStyle) + * ``` + * + * @param style - The style to get. + * + * @public */ + getStyleForNextShape(style: StyleProp): T { + const value = this.getInstanceState().stylesForNextShape[style.id] + return value === undefined ? style.defaultValue : (value as T) + } + + getShapeStyleIfExists(shape: TLShape, style: StyleProp): T | undefined { + const styleKey = this.styleProps[shape.type].get(style) + if (styleKey === undefined) return undefined + return getOwnProperty(shape.props, styleKey) as T | undefined + } + + /** + * A map of all the current styles either in the current selection, or that are relevant to the + * current tool. + * + * @example + * ```ts + * const color = editor.getSharedStyles().get(DefaultColorStyle) + * if (color && color.type === 'shared') { + * print('All selected shapes have the same color:', color.value) + * } + * ``` + * + * @public + */ + @computed({ isEqual: (a, b) => a.equals(b) }) + getSharedStyles(): ReadonlySharedStyleMap { + // If we're in selecting and if we have a selection, return the shared styles from the + // current selection + if (this.isIn('select') && this.getSelectedShapeIds().length > 0) { + return this._getSelectionSharedStyles() + } + + // If the current tool is associated with a shape, return the styles for that shape. + // Otherwise, just return an empty map. + const currentTool = this.root.getCurrent()! + const styles = new SharedStyleMap() + + if (!currentTool) return styles + + if (currentTool.shapeType) { + for (const style of this.styleProps[currentTool.shapeType].keys()) { + styles.applyValue(style, this.getStyleForNextShape(style)) + } + } + + return styles + } + + /** + * Get the currently selected shared opacity. + * If any shapes are selected, this returns the shared opacity of the selected shapes. + * Otherwise, this returns the chosen opacity for the next shape. + * + * @public + */ + @computed getSharedOpacity(): SharedStyle { + if (this.isIn('select') && this.getSelectedShapeIds().length > 0) { + let opacity: number | null = null + for (const shape of this._getStyleableShapes(this.getSelectedShapes())) { + if (opacity === null) { + opacity = shape.opacity + } else if (opacity !== shape.opacity) { + return { type: 'mixed' } + } + } + + if (opacity !== null) return { type: 'shared', value: opacity } + } + return { type: 'shared', value: this.getInstanceState().opacityForNextShape } + } + + /** + * Set the opacity for the next shapes. This will effect subsequently created shapes. + * + * @example + * ```ts + * editor.setOpacityForNextShapes(0.5) + * ``` + * + * @param opacity - The opacity to set. Must be a number between 0 and 1 inclusive. + * @param historyOptions - The history options for the change. + */ + setOpacityForNextShapes(opacity: number, historyOptions?: TLHistoryBatchOptions): this { + this.updateInstanceState({ opacityForNextShape: opacity }, historyOptions) + return this + } + + /** + * Set the current opacity. This will effect any selected shapes. + * + * @example + * ```ts + * editor.setOpacityForSelectedShapes(0.5) + * ``` + * + * @param opacity - The opacity to set. Must be a number between 0 and 1 inclusive. + */ + setOpacityForSelectedShapes(opacity: number): this { + const selectedShapes = this.getSelectedShapes() + + if (selectedShapes.length > 0) { + this.updateShapes( + this._getStyleableShapes(selectedShapes).map((shape) => ({ + id: shape.id, + type: shape.type, + opacity, + })) + ) + } + + return this + } + + /** + * Set the value of a {@link @tldraw/tlschema#StyleProp} for the next shapes. This change will be applied to subsequently created shapes. + * + * @example + * ```ts + * editor.setStyleForNextShapes(DefaultColorStyle, 'red') + * editor.setStyleForNextShapes(DefaultColorStyle, 'red', { ephemeral: true }) + * ``` + * + * @param style - The style to set. + * @param value - The value to set. + * @param historyOptions - The history options for the change. + * + * @public + */ + setStyleForNextShapes( + style: StyleProp, + value: T, + historyOptions?: TLHistoryBatchOptions + ): this { + const stylesForNextShape = this.getInstanceState().stylesForNextShape + + this.updateInstanceState( + { stylesForNextShape: { ...stylesForNextShape, [style.id]: value } }, + historyOptions + ) + + return this + } + + /** + * Set the value of a {@link @tldraw/tlschema#StyleProp}. This change will be applied to the currently selected shapes. + * + * @example + * ```ts + * editor.setStyleForSelectedShapes(DefaultColorStyle, 'red') + * ``` + * + * @param style - The style to set. + * @param value - The value to set. + * + * @public + */ + setStyleForSelectedShapes>(style: S, value: StylePropValue): this { + const selectedShapes = this.getSelectedShapes() + + if (selectedShapes.length > 0) { + const updates: TLShapePartial[] = [] + for (const shape of this._getStyleableShapes(selectedShapes)) { + const stylePropKey = this.styleProps[shape.type].get(style) + if (stylePropKey) { + updates.push({ + id: shape.id, + type: shape.type, + props: { [stylePropKey]: value }, + }) + } + } + + this.updateShapes(updates) + } + + return this + } + + /* --------------------- Content -------------------- */ + + /** @internal */ + externalAssetContentHandlers: { + [K in TLExternalAsset['type']]: { + [Key in K]: null | ((info: TLExternalAsset & { type: Key }) => Promise) + }[K] + } = { + file: null, + url: null, + } + + /** @internal */ + private readonly temporaryAssetPreview = new Map() + + /** + * Register an external asset handler. This handler will be called when the editor needs to + * create an asset for some external content, like an image/video file or a bookmark URL. For + * example, the 'file' type handler will be called when a user drops an image onto the canvas. + * + * The handler should extract any relevant metadata for the asset, upload it to blob storage + * using {@link Editor.uploadAsset} if needed, and return the asset with the metadata & uploaded + * URL. + * + * @example + * ```ts + * editor.registerExternalAssetHandler('file', myHandler) + * ``` + * + * @param type - The type of external content. + * @param handler - The handler to use for this content type. + * + * @public + */ + registerExternalAssetHandler( + type: T, + handler: null | ((info: TLExternalAsset & { type: T }) => Promise) + ): this { + this.externalAssetContentHandlers[type] = handler as any + return this + } + + /** + * Register a temporary preview of an asset. This is useful for showing a ghost image of + * something that is being uploaded. Retrieve the placeholder with + * {@link Editor.getTemporaryAssetPreview}. Placeholders last for 3 minutes by default, but this + * can be configured using + * + * @example + * ```ts + * editor.createTemporaryAssetPreview(assetId, file) + * ``` + * + * @param assetId - The asset's id. + * @param file - The raw file. + * + * @public + */ + createTemporaryAssetPreview(assetId: TLAssetId, file: File) { + if (this.temporaryAssetPreview.has(assetId)) { + return this.temporaryAssetPreview.get(assetId) + } + + const objectUrl = URL.createObjectURL(file) + this.temporaryAssetPreview.set(assetId, objectUrl) + + // eslint-disable-next-line no-restricted-globals -- we always want to revoke the asset and object URL + setTimeout(() => { + this.temporaryAssetPreview.delete(assetId) + URL.revokeObjectURL(objectUrl) + }, this.options.temporaryAssetPreviewLifetimeMs) + + return objectUrl + } + + /** + * Get temporary preview of an asset. This is useful for showing a ghost + * image of something that is being uploaded. + * + * @example + * ```ts + * editor.getTemporaryAssetPreview('someId') + * ``` + * + * @param assetId - The asset's id. + * + * @public + */ + getTemporaryAssetPreview(assetId: TLAssetId) { + return this.temporaryAssetPreview.get(assetId) + } + + /** + * Get an asset for an external asset content type. + * + * @example + * ```ts + * const asset = await editor.getAssetForExternalContent({ type: 'file', file: myFile }) + * const asset = await editor.getAssetForExternalContent({ type: 'url', url: myUrl }) + * ``` + * + * @param info - Info about the external content. + * @returns The asset. + */ + async getAssetForExternalContent(info: TLExternalAsset): Promise { + return await this.externalAssetContentHandlers[info.type]?.(info as any) + } + + hasExternalAssetHandler(type: TLExternalAsset['type']): boolean { + return !!this.externalAssetContentHandlers[type] + } + + /** @internal */ + externalContentHandlers: { + [K in TLExternalContent['type']]: { + [Key in K]: null | ((info: Extract, { type: Key }>) => void) + }[K] + } = { + text: null, + files: null, + 'file-replace': null, + embed: null, + 'svg-text': null, + url: null, + tldraw: null, + excalidraw: null, + } + + /** + * Register an external content handler. This handler will be called when the editor receives + * external content of the provided type. For example, the 'image' type handler will be called + * when a user drops an image onto the canvas. + * + * @example + * ```ts + * editor.registerExternalContentHandler('text', myHandler) + * ``` + * @example + * ```ts + * editor.registerExternalContentHandler<'embed', MyEmbedType>('embed', myHandler) + * ``` + * + * @param type - The type of external content. + * @param handler - The handler to use for this content type. + * + * @public + */ + registerExternalContentHandler['type'], E>( + type: T, + handler: + | null + | (( + info: T extends TLExternalContent['type'] + ? Extract, { type: T }> + : TLExternalContent + ) => void) + ): this { + this.externalContentHandlers[type] = handler as any + return this + } + + /** + * Handle external content, such as files, urls, embeds, or plain text which has been put into the app, for example by pasting external text or dropping external images onto canvas. + * + * @param info - Info about the external content. + * @param opts - Options for handling external content, including force flag to bypass readonly checks. + */ + async putExternalContent( + info: TLExternalContent, + opts = {} as { force?: boolean } + ): Promise { + if (!opts.force && this.getIsReadonly()) return + + return this.externalContentHandlers[info.type]?.(info as any) + } + + /** + * Handle replacing external content. + * + * @param info - Info about the external content. + * @param opts - Options for handling external content, including force flag to bypass readonly checks. + */ + async replaceExternalContent( + info: TLExternalContent, + opts = {} as { force?: boolean } + ): Promise { + if (!opts.force && this.getIsReadonly()) return + return this.externalContentHandlers[info.type]?.(info as any) + } + + /** + * Get content that can be exported for the given shape ids. + * + * @param shapes - The shapes (or shape ids) to get content for. + * + * @returns The exported content. + * + * @public + */ + getContentFromCurrentPage(shapes: TLShapeId[] | TLShape[]): TLContent | undefined { + // todo: make this work with any page, not just the current page + const ids = toShapeIds(shapes) + + if (!ids) return + if (ids.length === 0) return + + const shapeIds = this.getShapeAndDescendantIds(ids) + + return withIsolatedShapes(this, shapeIds, (bindingIdsToKeep) => { + const bindings: TLBinding[] = [] + for (const id of bindingIdsToKeep) { + const binding = this.getBinding(id) + if (!binding) continue + bindings.push(binding) + } + + const rootShapeIds: TLShapeId[] = [] + const shapes: TLShape[] = [] + for (const shapeId of shapeIds) { + const shape = this.getShape(shapeId) + if (!shape) continue + + const isRootShape = !shapeIds.has(shape.parentId as TLShapeId) + if (isRootShape) { + // Need to get page point and rotation of the shape because shapes in + // groups use local position/rotation + const pageTransform = this.getShapePageTransform(shape.id)! + const pagePoint = pageTransform.point() + shapes.push({ + ...shape, + x: pagePoint.x, + y: pagePoint.y, + rotation: pageTransform.rotation(), + parentId: this.getCurrentPageId(), + }) + rootShapeIds.push(shape.id) + } else { + shapes.push(shape) + } + } + + const assets: TLAsset[] = [] + const seenAssetIds = new Set() + for (const shape of shapes) { + if (!('assetId' in shape.props)) continue + + const assetId = shape.props.assetId + if (!assetId || seenAssetIds.has(assetId)) continue + + seenAssetIds.add(assetId) + const asset = this.getAsset(assetId) + if (!asset) continue + assets.push(asset) + } + + const users: TLUser[] = [] + const seenUserIds = new Set() + for (const userId of this._getReferencedUserIds(shapes)) { + const recordId = createUserId(userId) + if (seenUserIds.has(recordId)) continue + seenUserIds.add(recordId) + const user = this.store.get(recordId) + if (user) users.push(user) + } + + return { + schema: this.store.schema.serialize(), + shapes, + rootShapeIds, + bindings, + assets, + users, + } + }) + } + + async resolveAssetsInContent(content: TLContent | undefined): Promise { + if (!content) return undefined + + const assets: TLAsset[] = [] + await Promise.allSettled( + content.assets.map(async (asset) => { + if ( + (asset.type === 'image' || asset.type === 'video') && + !asset.props.src?.startsWith('data:image') && + !asset.props.src?.startsWith('data:video') && + !asset.props.src?.startsWith('http') + ) { + // If the asset can't be inlined (unresolvable src, fetch failure), fall through and + // keep the original record; dropping it leaves the pasted shapes pointing at an + // asset that doesn't exist + try { + const objectUrl = await this.store.props.assets.resolve(asset, { + screenScale: 1, + steppedScreenScale: 1, + dpr: 1, + networkEffectiveType: null, + shouldResolveToOriginal: true, + }) + if (objectUrl) { + // fetch resolves on 4xx/5xx, so without this check a 404 error page would be + // inlined as a data:text/html src + const response = await fetch(objectUrl) + if (response.ok) { + const assetWithDataUrl = structuredClone(asset as TLImageAsset | TLVideoAsset) + assetWithDataUrl.props.src = await FileHelpers.blobToDataUrl(await response.blob()) + assets.push(assetWithDataUrl) + return + } + console.warn(`Could not inline asset ${asset.id}: fetch returned ${response.status}`) + } + } catch (err) { + console.warn(`Could not inline asset ${asset.id}`, err) + } + } + assets.push(asset) + }) + ) + content.assets = assets + + return content + } + + /** + * Place content into the editor. + * + * @param content - The content. + * @param opts - Options for placing the content. + * + * @public + */ + putContentOntoCurrentPage( + content: TLContent, + opts: { + point?: VecLike + select?: boolean + preservePosition?: boolean + preserveIds?: boolean + } = {} + ): this { + if (this.getIsReadonly()) return this + + // todo: make this able to support putting content onto any page, not just the current page + + if (!content.schema) { + throw Error('Could not put content:\ncontent is missing a schema.') + } + + const { select = false, preserveIds = false, preservePosition = false } = opts + let { point = undefined } = opts + + // decide on a parent for the put shapes; if the parent is among the put shapes(?) then use its parent + + const currentPageId = this.getCurrentPageId() + const { rootShapeIds } = content + + // We need to collect the migrated records + const assets: TLAsset[] = [] + const shapes: TLShape[] = [] + const bindings: TLBinding[] = [] + const users: TLUser[] = [] + + // Let's treat the content as a store, and then migrate that store. + const store: StoreSnapshot = { + store: { + ...Object.fromEntries(content.assets.map((asset) => [asset.id, asset] as const)), + ...Object.fromEntries(content.shapes.map((shape) => [shape.id, shape] as const)), + ...Object.fromEntries( + content.bindings?.map((bindings) => [bindings.id, bindings] as const) ?? [] + ), + ...Object.fromEntries(content.users?.map((user) => [user.id, user] as const) ?? []), + }, + schema: content.schema, + } + const result = this.store.schema.migrateStoreSnapshot(store) + if (result.type === 'error') { + throw Error('Could not put content: could not migrate content') + } + for (const record of Object.values(result.value)) { + switch (record.typeName) { + case 'asset': { + assets.push(record) + break + } + case 'shape': { + shapes.push(record) + break + } + case 'binding': { + bindings.push(record) + break + } + case 'user': { + users.push(record) + break + } + } + } + + if (users.length > 0) { + const existingUserIds = new Set( + this.store + .allRecords() + .filter((r): r is TLUser => r.typeName === 'user') + .map((r) => r.id) + ) + const usersToCreate = users.filter((u) => !existingUserIds.has(u.id)) + if (usersToCreate.length > 0) { + this.store.put(usersToCreate) + } + } + + // Ok, we've got our migrated records, now we can continue! + const shapeIdMap = new Map( + preserveIds + ? shapes.map((shape) => [shape.id, shape.id]) + : shapes.map((shape) => [shape.id, createShapeId()]) + ) + const bindingIdMap = new Map( + preserveIds + ? bindings.map((binding) => [binding.id, binding.id]) + : bindings.map((binding) => [binding.id, createBindingId()]) + ) + + let pasteParentId: TLPageId | TLShapeId = currentPageId + + const shapesById = new Map(shapes.map((s) => [s.id, s])) + const rootShapesFromContent = compact(rootShapeIds.map((id) => shapesById.get(id))) + + if (point) { + // PASTE AT CURSOR: find the deepest accepts-children shape under the cursor + if (rootShapesFromContent.length > 0) { + const targetParent = this.getShapeAtPoint(point, { + hitInside: true, + hitFrameInside: true, + hitLocked: true, + filter: (shape) => { + const util = this.getShapeUtil(shape) + return rootShapesFromContent.every((rootShape) => + util.canReceiveNewChildrenOfType?.(shape, rootShape.type) + ) + }, + }) + pasteParentId = targetParent?.id ?? currentPageId + } + } else if (!preservePosition) { + // STANDARD PASTE: check if a selected shape (or its ancestor) accepts children + const selectedShapes = this.getSelectedShapes() + let selectedParent: TLShape | null = null + + const canAcceptAll = (candidate: TLShape) => { + const util = this.getShapeUtil(candidate) + return rootShapesFromContent.every((rs) => + util.canReceiveNewChildrenOfType?.(candidate, rs.type) + ) + } + + for (const shape of selectedShapes) { + // Find the nearest container: the shape itself if it can accept, + // an accepting ancestor, or fall back to the shape's parent + // (handles groups and other non-frame containers) + const candidate = canAcceptAll(shape) + ? shape + : (this.findShapeAncestor(shape, canAcceptAll) ?? + (isShapeId(shape.parentId) ? this.getShape(shape.parentId)! : null)) + + if (!candidate) { + selectedParent = null + break + } + if (!selectedParent) { + selectedParent = candidate + } else if (selectedParent.id !== candidate.id) { + // Different candidates — find the deepest common accepting ancestor + const spAncestors = this.getShapeAncestors(selectedParent) + if (canAcceptAll(selectedParent)) spAncestors.push(selectedParent) + const acceptingAncestors = spAncestors.filter(canAcceptAll) + + const candidateAncestorIds = new Set([ + candidate.id, + ...this.getShapeAncestors(candidate).map((a) => a.id), + ]) + + let common: TLShape | null = null + for (let i = acceptingAncestors.length - 1; i >= 0; i--) { + if (candidateAncestorIds.has(acceptingAncestors[i].id)) { + common = acceptingAncestors[i] + break + } + } + + selectedParent = common + if (!selectedParent) break + } + } + + // Don't paste a shape into itself (the duplicating-a-frame case) + if (selectedParent && shapeIdMap.has(selectedParent.id)) { + selectedParent = null + } + + if (selectedParent) { + pasteParentId = selectedParent.id + } + } + + let index = this.getHighestIndexForParent(pasteParentId) // todo: requires that the putting page is the current page + + const rootShapes: TLShape[] = [] + + const newShapes: TLShape[] = shapes.map((oldShape): TLShape => { + const newId = shapeIdMap.get(oldShape.id)! + + // Create the new shape (new except for the id) + let newShape = { ...oldShape, id: newId } + + // Give the shape util a chance to modify the copy, e.g. to re-stamp note + // attribution to the current user so we don't forge the original author's identity. + // When ids are preserved the shape keeps its identity (e.g. moveShapesToPage + // relocating it), so it's not a duplicate and the hook must not run. + if (!preserveIds) { + newShape = this.getShapeUtil(newShape).onBeforeDuplicate?.(oldShape, newShape) ?? newShape + } + + if (rootShapeIds.includes(oldShape.id)) { + newShape.parentId = currentPageId + rootShapes.push(newShape) + } + + // Assign the child to its new parent. + + // If the child's parent is among the putting shapes, then assign + // it to the new parent's id. + if (shapeIdMap.has(newShape.parentId)) { + newShape.parentId = shapeIdMap.get(oldShape.parentId)! + } else { + // newShape.parentId = pasteParentId + newShape.index = index + index = getIndexAbove(index) + } + + return newShape + }) + + if (newShapes.length + this.getCurrentPageShapeIds().size > this.options.maxShapesPerPage) { + // There's some complexity here involving children + // that might be created without their parents, so + // if we're going over the limit then just don't paste. + alertMaxShapes(this) + return this + } + + const newBindings = bindings.map( + (oldBinding): TLBinding => ({ + ...oldBinding, + id: assertExists(bindingIdMap.get(oldBinding.id)), + fromId: assertExists(shapeIdMap.get(oldBinding.fromId)), + toId: assertExists(shapeIdMap.get(oldBinding.toId)), + }) + ) + + // These are all the assets we need to create + const assetsToCreate: TLAsset[] = [] + + // These assets have base64 data that may need to be hosted + const assetsToUpdate: (TLImageAsset | TLVideoAsset)[] = [] + + for (const asset of assets) { + if (this.store.has(asset.id)) { + // We already have this asset + continue + } + + if ( + (asset.type === 'image' && asset.props.src?.startsWith('data:image')) || + (asset.type === 'video' && asset.props.src?.startsWith('data:video')) + ) { + // it's src is a base64 image or video; we need to create a new asset without the src, + // then create a new asset from the original src. So we keep the original asset for the + // upload and create a copy with its src removed. Copy rather than mutate: when no + // migration applies, migrateStoreSnapshot hands back the caller's own record objects. + assetsToUpdate.push(asset as TLImageAsset | TLVideoAsset) + const assetWithoutSrc = structuredClone(asset as TLImageAsset | TLVideoAsset) + assetWithoutSrc.props.src = null + assetsToCreate.push(assetWithoutSrc) + continue + } + + // Add the asset to the list of assets to create + assetsToCreate.push(asset) + } + + // Start loading the new assets, order does not matter + Promise.allSettled( + (assetsToUpdate as (TLImageAsset | TLVideoAsset)[]).map(async (asset) => { + // Turn the data url into a file + const file = await dataUrlToFile( + asset.props.src!, + asset.props.name, + asset.props.mimeType ?? 'image/png' + ) + + // Get a new asset for the file + const newAsset = await this.getAssetForExternalContent({ + type: 'file', + file, + assetId: asset.id, + }) + + if (!newAsset) { + // If we don't have a new asset, delete the old asset. + // The shapes that reference this asset should break. + this.deleteAssets([asset.id]) + return + } + + // Save the new asset under the old asset's id + this.updateAssets([{ ...newAsset, id: asset.id }]) + }) + ) + + this.run(() => { + if (assetsToCreate.length > 0) this.createAssets(assetsToCreate) + this.createShapes(newShapes) + this.createBindings(newBindings) + if (select) this.select(...rootShapes.map((s) => s.id)) + + // Reparent root shapes to paste parent if not page + if (pasteParentId !== currentPageId) { + this.reparentShapes( + rootShapes.map((s) => s.id), + pasteParentId + ) + } + + // --- POSITIONING --- + const rootBounds = Box.Common(compact(rootShapes.map((s) => this.getShapePageBounds(s.id)))) + + if (point === undefined) { + if (!isPageId(pasteParentId)) { + // Paste into selected parent → center in that shape + const shape = this.getShape(pasteParentId)! + point = Mat.applyToPoint( + this.getShapePageTransform(shape), + this.getShapeGeometry(shape).bounds.center + ) + } else if (preservePosition) { + // preservePosition (page duplication) → keep original coords + point = rootBounds.center + } else { + // Standard paste to page: check viewport overlap + const viewportPageBounds = this.getViewportPageBounds() + const anyOverlap = rootShapes.some((s) => { + const b = this.getShapePageBounds(s.id) + return b && viewportPageBounds.collides(b) + }) + point = anyOverlap ? rootBounds.center : viewportPageBounds.center + } + } + + // Apply offset to move shapes to target point + const pageCenter = Box.Common( + compact(rootShapes.map(({ id }) => this.getShapePageBounds(id))) + ).center + const offset = Vec.Sub(point, pageCenter) + + if (offset.x !== 0 || offset.y !== 0) { + this.updateShapes( + rootShapes.map(({ id }) => { + const s = this.getShape(id)! + const localRotation = this.getShapeParentTransform(id).decompose().rotation + const localDelta = Vec.Rot(offset, -localRotation) + return { id: s.id, type: s.type, x: s.x + localDelta.x, y: s.y + localDelta.y } + }) + ) + } + + // Auto-reparent into frames when pasted to page level + if (isPageId(pasteParentId)) { + const currentRootShapes = compact(rootShapes.map((s) => this.getShape(s.id))) + // Don't reparent into source shapes (prevents a duplicate frame + // from being swallowed by its original), and require the shape's + // center to be inside the parent (filters out edge-only overlaps) + const { reparenting } = getDroppedShapesToNewParents( + this, + currentRootShapes, + (shape, parent) => { + if (shapeIdMap.has(parent.id)) return false + const shapeBounds = this.getShapePageBounds(shape) + const parentBounds = this.getShapePageBounds(parent) + if (!shapeBounds || !parentBounds) return false + return parentBounds.containsPoint(shapeBounds.center) + } + ) + reparenting.forEach((childrenToReparent, newParentId) => { + if (childrenToReparent.length === 0) return + this.reparentShapes( + childrenToReparent.map((s) => s.id), + newParentId + ) + }) + } + + // Kick out any pasted root shapes that ended up outside their parent container + const newShapeIdSet = new Set(newShapes.map((s) => s.id)) + const shapesToKickout = rootShapes + .map((s) => s.id) + .filter((id) => { + const shape = this.getShape(id) + if (!shape) return false + // Only check shapes that are children of a shape (not page) + if (isPageId(shape.parentId)) return false + // Don't check containers with pasted children (preserves intentional + // parent-child relationships from the copied content) + const children = this.getSortedChildIdsForParent(id) + return !children.some((childId) => newShapeIdSet.has(childId)) + }) + + if (shapesToKickout.length > 0) { + kickoutOccludedShapes(this, shapesToKickout) + } + }) + + return this + } + + /** + * Get an exported SVG element of the given shapes. + * + * @param shapes - The shapes (or shape ids) to export. + * @param opts - Options for the export. + * + * @returns The SVG element. + * + * @public + */ + async getSvgElement(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) { + const ids = shapes.length === 0 ? this.getCurrentPageShapeIdsSorted() : toShapeIds(shapes) + + if (ids.length === 0) return undefined + + // Text geometry is measured from the loaded font, so the export's bounds - and the + // layout of any text within it - depend on the right fonts having loaded. Wait for them + // before we measure; otherwise an export taken before fonts finish loading (e.g. right + // after the editor mounts) is sized and laid out with fallback-font metrics. + await this.fonts.loadRequiredFontsForCurrentPage(this.options.maxFontsToLoadBeforeRender) + + return exportToSvg(this, ids, opts) + } + + /** + * Get an exported SVG string of the given shapes. + * + * @param shapes - The shapes (or shape ids) to export. + * @param opts - Options for the export. + * + * @returns The SVG element. + * + * @public + */ + async getSvgString(shapes: TLShapeId[] | TLShape[], opts: TLSvgExportOptions = {}) { + const result = await this.getSvgElement(shapes, opts) + if (!result) return undefined + + const serializer = new XMLSerializer() + return { + svg: serializer.serializeToString(result.svg), + width: result.width, + height: result.height, + trimPadding: result.trimPadding, + } + } + + /** + * Get an exported image of the given shapes. + * + * @param shapes - The shapes (or shape ids) to export. + * @param opts - Options for the export. + * + * @returns A blob of the image. + * @public + */ + async toImage(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) { + const withDefaults = { + format: 'png', + scale: 1, + pixelRatio: opts.format === 'svg' ? undefined : 2, + ...opts, + } satisfies TLImageExportOptions + const result = await this.getSvgString(shapes, withDefaults) + if (!result) throw new Error('Could not create SVG') + + switch (withDefaults.format) { + case 'svg': { + let svg = result.svg + let w = result.width + let h = result.height + if (result.trimPadding > 0) { + const trimmed = await trimSvgToContent(svg, { + width: w, + height: h, + trimPadding: result.trimPadding, + scale: withDefaults.scale, + }) + if (trimmed) { + svg = trimmed.svg + w = trimmed.width + h = trimmed.height + } + } + return { + blob: new Blob([svg], { type: 'image/svg+xml' }), + width: w, + height: h, + } + } + case 'jpeg': + case 'png': + case 'webp': { + const imageResult = await getSvgAsImageWithOptions(result.svg, { + type: withDefaults.format, + quality: withDefaults.quality, + pixelRatio: withDefaults.pixelRatio, + width: result.width, + height: result.height, + trimPadding: result.trimPadding, + scale: withDefaults.scale, + }) + if (!imageResult) { + throw new Error('Could not construct image.') + } + return imageResult + } + default: { + exhaustiveSwitchError(withDefaults.format) + } + } + } + + /** + * Get an exported image of the given shapes as a data URL. + * + * @param shapes - The shapes (or shape ids) to export. + * @param opts - Options for the export. + * + * @returns A data URL of the image. + * @public + */ + async toImageDataUrl(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) { + const { blob, width, height } = await this.toImage(shapes, opts) + return { + url: await FileHelpers.blobToDataUrl(blob), + width, + height, + } + } + + /* --------------------- Events --------------------- */ + + /** + * Dispatch a cancel event. + * + * @example + * ```ts + * editor.cancel() + * ``` + * + * @public + */ + cancel(): this { + this.dispatch({ type: 'misc', name: 'cancel' }) + return this + } + + /** + * Dispatch an interrupt event. + * + * @example + * ```ts + * editor.interrupt() + * ``` + * + * @public + */ + interrupt(): this { + this.dispatch({ type: 'misc', name: 'interrupt' }) + return this + } + + /** + * Dispatch a complete event. + * + * @example + * ```ts + * editor.complete() + * ``` + * + * @public + */ + complete(): this { + this.dispatch({ type: 'misc', name: 'complete' }) + return this + } + + /** + * Dispatch a pointer move event in the current position of the pointer. This is useful when + * external circumstances have changed (e.g. the camera moved or a shape was moved) and you want + * the current interaction to respond to that change. + * + * @example + * ```ts + * editor.updatePointer() + * ``` + * + * @param options - The options for updating the pointer. + * @returns The editor instance. + * @public + */ + updatePointer(options?: TLUpdatePointerOptions): this { + const event: TLPointerEventInfo = { + type: 'pointer', + target: 'canvas', + name: 'pointer_move', + point: + options?.point ?? + // weird but true: what `inputs` calls screen-space is actually viewport space. so + // we need to convert back into true screen space first. we should fix this... + Vec.Add( + this.inputs.getCurrentScreenPoint(), + this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds + ), + pointerId: options?.pointerId ?? 0, + button: options?.button ?? 0, + isPen: options?.isPen ?? this.inputs.getIsPen(), + shiftKey: options?.shiftKey ?? this.inputs.getShiftKey(), + altKey: options?.altKey ?? this.inputs.getAltKey(), + ctrlKey: options?.ctrlKey ?? this.inputs.getCtrlKey(), + metaKey: options?.metaKey ?? this.inputs.getMetaKey(), + accelKey: false, + } + + // needs to be calculated second + event.accelKey = options?.accelKey ?? this.inputs.getAccelKey() + + if (options?.immediate) { + this._flushEventForTick(event) + } else { + this.dispatch(event) + } + + return this + } + + /** + * Puts the editor into focused mode. + * + * This makes the editor eligible to receive keyboard events and some pointer events (move, wheel). + * + * @example + * ```ts + * editor.focus() + * ``` + * + * By default this also dispatches a 'focus' event to the container element. To prevent this, pass `focusContainer: false`. + * + * @example + * ```ts + * editor.focus({ focusContainer: false }) + * ``` + * + * @public + */ + focus({ focusContainer = true } = {}): this { + if (this.getIsFocused()) return this + if (focusContainer) this.focusManager.focus() + this.updateInstanceState({ isFocused: true }) + return this + } + + /** + * Switches off the editor's focused mode. + * + * This makes the editor ignore keyboard events and some pointer events (move, wheel). + * + * @example + * ```ts + * editor.blur() + * ``` + * By default this also dispatches a 'blur' event to the container element. To prevent this, pass `blurContainer: false`. + * + * @example + * ```ts + * editor.blur({ blurContainer: false }) + * ``` + * + * @public + */ + blur({ blurContainer = true } = {}): this { + if (!this.getIsFocused()) return this + this.focusManager.blur({ blurContainer }) + this.updateInstanceState({ isFocused: false }) + return this + } + + /** + * @public + * @returns true if the editor is focused + */ + @computed getIsFocused() { + return this.getInstanceState().isFocused + } + + /** + * @public + * @returns true if the editor is in readonly mode + */ + @computed getIsReadonly() { + return this.getInstanceState().isReadonly + } + + /** + * @public + * @returns a snapshot of the store's UI and document state + */ + getSnapshot() { + return getSnapshot(this.store) + } + + /** + * Loads a snapshot into the editor. + * @param snapshot - The snapshot to load. + * @param opts - The options for loading the snapshot. + * @returns + */ + loadSnapshot( + snapshot: Partial | TLStoreSnapshot, + opts?: TLLoadSnapshotOptions + ) { + loadSnapshot(this.store, snapshot, opts) + return this + } + + private _zoomToFitPageContentAt100Percent() { + const bounds = this.getCurrentPageBounds() + if (bounds) { + this.zoomToBounds(bounds, { immediate: true, targetZoom: this.getBaseZoom() }) + } + } + private _navigateToDeepLink(deepLink: TLDeepLink) { + this.run(() => { + switch (deepLink.type) { + case 'page': { + const page = this.getPage(deepLink.pageId) + if (page) { + this.setCurrentPage(page) + } + this._zoomToFitPageContentAt100Percent() + return + } + case 'shapes': { + const allShapes = compact(deepLink.shapeIds.map((id) => this.getShape(id))) + const byPage: { [pageId: string]: TLShape[] } = {} + for (const shape of allShapes) { + const pageId = this.getAncestorPageId(shape) + if (!pageId) continue + byPage[pageId] ??= [] + byPage[pageId].push(shape) + } + const [pageId, shapes] = Object.entries(byPage).sort( + ([_, a], [__, b]) => b.length - a.length + )[0] ?? ['', []] + + if (!pageId || !shapes.length) { + this._zoomToFitPageContentAt100Percent() + } else { + this.setCurrentPage(pageId as TLPageId) + const bounds = Box.Common(shapes.map((s) => this.getShapePageBounds(s)!)) + this.zoomToBounds(bounds, { immediate: true, targetZoom: this.getBaseZoom() }) + } + return + } + case 'viewport': { + if (deepLink.pageId) { + if (!this.getPage(deepLink.pageId)) { + this._zoomToFitPageContentAt100Percent() + return + } + this.setCurrentPage(deepLink.pageId) + } + this.zoomToBounds(deepLink.bounds, { immediate: true, inset: 0 }) + return + } + default: + exhaustiveSwitchError(deepLink) + } + }) + } + + /** + * Handles navigating to the content specified by the query param in the given URL. + * + * Use {@link Editor.createDeepLink} to create a URL with a deep link query param. + * + * If no URL is provided, it will look for the param in the current `window.location.href`. + * + * @example + * ```ts + * editor.navigateToDeepLink() + * ``` + * + * The default parameter name is 'd'. You can override this by providing the `param` option. + * + * @example + * ```ts + * // disable page parameter and change viewport parameter to 'c' + * editor.navigateToDeepLink({ + * param: 'x', + * url: 'https://my-app.com/my-document?x=200.12.454.23.xyz123', + * }) + * ``` + * + * @param opts - Options for loading the state from the URL. + */ + navigateToDeepLink(opts?: TLDeepLink | { url?: string | URL; param?: string }): Editor { + if (opts && 'type' in opts) { + this._navigateToDeepLink(opts) + return this + } + + const url = new URL(opts?.url ?? window.location.href) + const deepLinkString = url.searchParams.get(opts?.param ?? 'd') + + if (!deepLinkString) { + this._zoomToFitPageContentAt100Percent() + return this + } + + try { + this._navigateToDeepLink(parseDeepLinkString(deepLinkString)) + } catch (e) { + console.warn(e) + this._zoomToFitPageContentAt100Percent() + } + return this + } + + /** + * Turns the given URL into a deep link by adding a query parameter. + * + * e.g. `https://my-app.com/my-document?d=100.100.200.200.xyz123` + * + * If no URL is provided, it will use the current `window.location.href`. + * + * @example + * ```ts + * // create a deep link to the current page + viewport + * navigator.clipboard.writeText(editor.createDeepLink()) + * ``` + * + * You can link to a particular set of shapes by providing a `to` parameter. + * + * @example + * ```ts + * // create a deep link to the set of currently selected shapes + * navigator.clipboard.writeText(editor.createDeepLink({ + * to: { type: 'selection', shapeIds: editor.getSelectedShapeIds() } + * })) + * ``` + * + * The default query param is 'd'. You can override this by providing a `param` parameter. + * + * @example + * ```ts + * // Use `x` as the param name instead + * editor.createDeepLink({ param: 'x' }) + * ``` + * + * @param opts - Options for adding the state to the URL. + * @returns the updated URL + */ + createDeepLink(opts?: { url?: string | URL; param?: string; to?: TLDeepLink }): URL { + const url = new URL(opts?.url ?? window.location.href) + + url.searchParams.set( + opts?.param ?? 'd', + createDeepLinkString( + opts?.to ?? { + type: 'viewport', + pageId: this.options.maxPages === 1 ? undefined : this.getCurrentPageId(), + bounds: this.getViewportPageBounds(), + } + ) + ) + + return url + } + + /** + * Register a listener for changes to a deep link for the current document. + * + * You'll typically want to use this indirectly via the {@link TldrawEditorBaseProps.deepLinks} prop on the `` component. + * + * By default this will update `window.location` in place, but you can provide a custom callback + * to handle state changes on your own. + * + * @example + * ```ts + * editor.registerDeepLinkListener({ + * onChange(url) { + * window.history.replaceState({}, document.title, url.toString()) + * } + * }) + * ``` + * + * You can also provide a custom URL to update, in which case you must also provide `onChange`. + * + * @example + * ```ts + * editor.registerDeepLinkListener({ + * getUrl: () => `https://my-app.com/my-document`, + * onChange(url) { + * setShareUrl(url.toString()) + * } + * }) + * ``` + * + * By default this will update with a debounce interval of 500ms, but you can provide a custom interval. + * + * @example + * ```ts + * editor.registerDeepLinkListener({ debounceMs: 1000 }) + * ``` + * The default parameter name is `d`. You can override this by providing a `param` option. + * + * @example + * ```ts + * editor.registerDeepLinkListener({ param: 'x' }) + * ``` + * @param opts - Options for setting up the listener. + * @returns a function that will stop the listener. + */ + registerDeepLinkListener(opts?: TLDeepLinkOptions): () => void { + if (opts?.getUrl && !opts?.onChange) { + throw Error( + '[tldraw:urlStateSync] If you specify getUrl, you must also specify the onChange callback.' + ) + } + + const url$ = computed('url with state', () => { + const url = opts?.getUrl?.(this) ?? window.location.href + const urlWithState = this.createDeepLink({ + param: opts?.param, + url, + to: opts?.getTarget?.(this), + }) + return urlWithState.toString() + }) + + const announceChange = + opts?.onChange ?? + (() => { + const url = this.createDeepLink({ + param: opts?.param, + to: opts?.getTarget?.(this), + }) + + window.history.replaceState({}, this.getContainerDocument().title, url.toString()) + }) + + const scheduleEffect = debounce((execute: () => void) => execute(), opts?.debounceMs ?? 500) + + const unlisten = react( + 'update url on state change', + () => announceChange(new URL(url$.get()), this), + { scheduleEffect } + ) + + return () => { + unlisten() + scheduleEffect.cancel() + } + } + + /** + * A manager for recording multiple click events. + * + * @internal + */ + protected _clickManager = new ClickManager(this) + + /** + * Prevent a double click event from firing the next time the user clicks + * + * @public + */ + cancelDoubleClick() { + this._clickManager.cancelDoubleClickTimeout() + } + + /** + * The previous cursor. Used for restoring the cursor after pan events. + * + * @internal + */ + private _prevCursor: TLCursorType = 'default' + + /** @internal */ + private _shiftKeyTimeout = -1 as any + + /** + * Release the shift modifier: clear its debounce timer id, drop the atom, and + * dispatch a synthetic `key_up` so `inputs.keys` and tool `onKeyUp` handlers update. + * Runs both as the 150ms debounce timer callback and when a pointer down flushes it. + * @internal + */ + @bind + _releaseShiftKey() { + this._shiftKeyTimeout = -1 + this.inputs.setShiftKey(false) + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Shift', + shiftKey: this.inputs.getShiftKey(), + ctrlKey: this.inputs.getCtrlKey(), + altKey: this.inputs.getAltKey(), + metaKey: this.inputs.getMetaKey(), + accelKey: this.inputs.getAccelKey(), + code: 'ShiftLeft', + }) + } + + /** @internal */ + private _altKeyTimeout = -1 as any + + /** + * Release the alt modifier. See {@link Editor._releaseShiftKey}. + * @internal + */ + @bind + _releaseAltKey() { + this._altKeyTimeout = -1 + this.inputs.setAltKey(false) + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Alt', + shiftKey: this.inputs.getShiftKey(), + ctrlKey: this.inputs.getCtrlKey(), + altKey: this.inputs.getAltKey(), + metaKey: this.inputs.getMetaKey(), + accelKey: this.inputs.getAccelKey(), + code: 'AltLeft', + }) + } + + /** @internal */ + private _ctrlKeyTimeout = -1 as any + + /** + * Release the ctrl modifier. See {@link Editor._releaseShiftKey}. + * @internal + */ + @bind + _releaseCtrlKey() { + this._ctrlKeyTimeout = -1 + this.inputs.setCtrlKey(false) + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Ctrl', + shiftKey: this.inputs.getShiftKey(), + ctrlKey: this.inputs.getCtrlKey(), + altKey: this.inputs.getAltKey(), + metaKey: this.inputs.getMetaKey(), + accelKey: this.inputs.getAccelKey(), + code: 'ControlLeft', + }) + } + + /** @internal */ + private _metaKeyTimeout = -1 as any + + /** + * Release the meta modifier. See {@link Editor._releaseShiftKey}. + * @internal + */ + @bind + _releaseMetaKey() { + this._metaKeyTimeout = -1 + this.inputs.setMetaKey(false) + this.dispatch({ + type: 'keyboard', + name: 'key_up', + key: 'Meta', + shiftKey: this.inputs.getShiftKey(), + ctrlKey: this.inputs.getCtrlKey(), + altKey: this.inputs.getAltKey(), + metaKey: this.inputs.getMetaKey(), + accelKey: this.inputs.getAccelKey(), + code: 'MetaLeft', + }) + } + + /** + * Flush any modifier still sitting in its 150ms release-debounce window, so a new + * interaction (a pointer down) starts with correct modifier state instead of a + * just-released key still being counted as held. A pending timer is exactly + * "released but still counted as held"; a genuinely-held modifier has no timer + * (-1) and is left alone. + * + * Each modifier is released through the same path its timer would have taken + * (`_releaseShiftKey` and friends), which dispatches the synthetic `key_up` so + * stale codes (e.g. `ShiftLeft`) leave `inputs.keys` and tool `onKeyUp` handlers run. + * We clear every pending modifier atom and timer *first*, then dispatch: a synthetic + * `key_up` reports all currently-held modifiers, so releasing them one at a time + * would make each event re-confirm the not-yet-released ones and cancel their flush. + * @internal + */ + private _releaseDebouncedModifiers() { + const releaseShift = this._shiftKeyTimeout !== -1 + const releaseAlt = this._altKeyTimeout !== -1 + const releaseCtrl = this._ctrlKeyTimeout !== -1 + const releaseMeta = this._metaKeyTimeout !== -1 + + if (releaseShift) { + this.timers.clearTimeout(this._shiftKeyTimeout) + this._shiftKeyTimeout = -1 + this.inputs.setShiftKey(false) + } + if (releaseAlt) { + this.timers.clearTimeout(this._altKeyTimeout) + this._altKeyTimeout = -1 + this.inputs.setAltKey(false) + } + if (releaseCtrl) { + this.timers.clearTimeout(this._ctrlKeyTimeout) + this._ctrlKeyTimeout = -1 + this.inputs.setCtrlKey(false) + } + if (releaseMeta) { + this.timers.clearTimeout(this._metaKeyTimeout) + this._metaKeyTimeout = -1 + this.inputs.setMetaKey(false) + } + + // Dispatch the synthetic key_up for each flushed modifier (same path its 150ms + // timer would run): clears stale codes from inputs.keys and fires tool onKeyUp. + if (releaseShift) this._releaseShiftKey() + if (releaseAlt) this._releaseAltKey() + if (releaseCtrl) this._releaseCtrlKey() + if (releaseMeta) this._releaseMetaKey() + } + + /** @internal */ + private _restoreToolId = 'select' + + /** @internal */ + private _didPinch = false + + /** @internal */ + private _selectedShapeIdsAtPointerDown: TLShapeId[] = [] + + /** + * Whether `_selectedShapeIdsAtPointerDown` holds a pre-gesture selection + * captured by a `pointer_down` (the touch path) that a following pinch + * should restore. False when no pointer_down preceded the pinch (the + * Safari trackpad path uses gesture events), in which case `pinch_start` + * captures the live selection instead. + * @internal + */ + private _didCaptureSelectionAtPointerDown = false + + /** @internal */ + private _longPressTimeout = -1 as any + + /** @internal */ + capturedPointerId: number | null = null + + /** @internal */ + private readonly performanceTracker: PerformanceTracker + + /** @internal */ + private performanceTrackerTimeout = -1 as any + + /** @internal */ + private handledEvents = new WeakSet() + + /** + * In tldraw, events are sometimes handled by multiple components. For example, the shapes might + * have events, but the canvas handles events too. The way that the canvas handles events can + * interfere with the with the shapes event handlers - for example, it calls `.preventDefault()` + * on `pointerDown`, which also prevents `click` events from firing on the shapes. + * + * You can use `.stopPropagation()` to prevent the event from propagating to the rest of the + * DOM, but that can impact non-tldraw event handlers set up elsewhere. By using + * `markEventAsHandled`, you'll stop other parts of tldraw from handling the event without + * impacting other, non-tldraw event handlers. See also {@link Editor.wasEventAlreadyHandled}. + * + * @public + */ + markEventAsHandled(e: Event | { nativeEvent: Event }) { + const nativeEvent = 'nativeEvent' in e ? e.nativeEvent : e + this.handledEvents.add(nativeEvent) + } + + /** + * Checks if an event has already been handled. See {@link Editor.markEventAsHandled}. + * + * @public + */ + wasEventAlreadyHandled(e: Event | { nativeEvent: Event }) { + const nativeEvent = 'nativeEvent' in e ? e.nativeEvent : e + return this.handledEvents.has(nativeEvent) + } + + /** + * Dispatch an event to the editor. + * + * @example + * ```ts + * editor.dispatch(myPointerEvent) + * ``` + * + * @param info - The event info. + * + * @public + */ + dispatch(info: TLEventInfo) { + this._pendingEventsForNextTick.push(info) + if ( + !( + (info.type === 'pointer' && info.name === 'pointer_move') || + info.type === 'wheel' || + info.type === 'pinch' + ) + ) { + this._flushEventsForTick(0) + } + return this + } + + private _pendingEventsForNextTick: TLEventInfo[] = [] + + private _flushEventsForTick(elapsed: number) { + this.run(() => { + if (this._pendingEventsForNextTick.length > 0) { + const events = [...this._pendingEventsForNextTick] + this._pendingEventsForNextTick.length = 0 + for (const info of events) { + this._flushEventForTick(info) + } + } + if (elapsed > 0) { + this.root.handleEvent({ type: 'misc', name: 'tick', elapsed }) + } + this.scribbles.tick(elapsed) + }) + } + + _flushEventForTick(info: TLEventInfo) { + // prevent us from spamming similar event errors if we're crashed. + // todo: replace with new readonly mode? + if (this.getCrashingError()) return this + + this.emit('before-event', info) + + const { inputs } = this + const { type } = info + + if (info.type === 'misc') { + // stop panning if the interaction is cancelled or completed + if (info.name === 'cancel' || info.name === 'complete') { + this.inputs.setIsDragging(false) + + // A pan owned by a held spacebar outlives the cancelled interaction; + // key_up ends it. Otherwise Escape mid-pan stops the camera until + // the user releases and re-presses Space (#10446). + if (this.inputs.getIsPanning() && !this.inputs.keys.has('Space')) { + this.inputs.setIsPanning(false) + this.inputs.setIsSpacebarPanning(false) + this.setCursor({ type: this._prevCursor, rotation: 0 }) + } + } + + this.root.handleEvent(info) + this.emit('event', info) + return + } + + if (info.shiftKey) { + this.timers.clearTimeout(this._shiftKeyTimeout) + this._shiftKeyTimeout = -1 + inputs.setShiftKey(true) + } else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) { + this._shiftKeyTimeout = this.timers.setTimeout(this._releaseShiftKey, 150) + } + + if (info.altKey) { + this.timers.clearTimeout(this._altKeyTimeout) + this._altKeyTimeout = -1 + inputs.setAltKey(true) + } else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) { + this._altKeyTimeout = this.timers.setTimeout(this._releaseAltKey, 150) + } + + if (info.ctrlKey) { + this.timers.clearTimeout(this._ctrlKeyTimeout) + this._ctrlKeyTimeout = -1 + inputs.setCtrlKey(true) + } else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) { + this._ctrlKeyTimeout = this.timers.setTimeout(this._releaseCtrlKey, 150) + } + + if (info.metaKey && info.name !== 'key_up') { + // Unlike the other modifiers, the native metaKey property is still true on keyup. + // If we don't have this guard, then the metakey will be left true without the timeout. + this.timers.clearTimeout(this._metaKeyTimeout) + this._metaKeyTimeout = -1 + inputs.setMetaKey(true) + } else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) { + this._metaKeyTimeout = this.timers.setTimeout(this._releaseMetaKey, 150) + } + + if (!inputs.getIsPointing()) { + inputs.setIsDragging(false) + } + + const instanceState = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + const pageState = this.store.get(this._getCurrentPageStateId())! + const cameraOptions = this._cameraOptions.__unsafe__getWithoutCapture()! + + switch (type) { + case 'pinch': { + this.timers.clearTimeout(this._longPressTimeout) + this.inputs.updateFromEvent(info) + + switch (info.name) { + case 'pinch_start': { + if (inputs.getIsPinching()) return + + if (!inputs.getIsEditing()) { + // If a pointer_down already captured the pre-gesture selection, + // keep it: on touch, the first finger's pointer_down can change + // the selection before the second finger starts the pinch, and we + // want to restore the selection from before that change. When no + // pointer_down preceded the pinch (Safari delivers trackpad pinches + // as gesture events), capture the live selection now. + if (!this._didCaptureSelectionAtPointerDown) { + this._selectedShapeIdsAtPointerDown = [...pageState.selectedShapeIds] + } + + this._didPinch = true + + inputs.setIsPinching(true) + + this.interrupt() + + // If the first finger changed the selection, roll it back now rather + // than waiting for the pinch to end, so the pre-gesture selection is + // what's shown during the pinch. + if (this._didCaptureSelectionAtPointerDown) { + this.setSelectedShapes(this._selectedShapeIdsAtPointerDown) + } + } + + this.emit('event', info) + return // Stop here! + } + case 'pinch': { + if (!inputs.getIsPinching()) return + // Lock the zoom, not the gesture: skipping pinch_start would send the fingers' + // pointer events to the tool, and skipping pinch_end would leave isPinching stuck. + if (cameraOptions.isLocked) return + + const { + point: { z = 1 }, + delta: { x: dx, y: dy }, + } = info + + // The center of the pinch in screen space + const { x, y } = Vec.SubXY( + info.point, + instanceState.screenBounds.x, + instanceState.screenBounds.y + ) + + this.stopCameraAnimation() + if (instanceState.followingUserId) { + this.stopFollowingUser() + } + + const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera()) + + const { panSpeed } = cameraOptions + this._setCamera( + new Vec( + cx + (dx * panSpeed) / cz - x / cz + x / z, + cy + (dy * panSpeed) / cz - y / cz + y / z, + z + ), + { immediate: true } + ) + + this.performance._notifyCameraOperation('zooming') + this.emit('event', info) + return // Stop here! + } + case 'pinch_end': { + if (!inputs.getIsPinching()) return this + + // Stop pinching + inputs.setIsPinching(false) + + // Stash and clear the shapes that were selected when the pinch started + const { _selectedShapeIdsAtPointerDown: shapesToReselect } = this + this.setSelectedShapes(this._selectedShapeIdsAtPointerDown) + this._selectedShapeIdsAtPointerDown = [] + this._didCaptureSelectionAtPointerDown = false + + if (this._didPinch) { + this._didPinch = false + if (shapesToReselect.length > 0) { + this.once('tick', () => { + if (!this._didPinch) { + // Unless we've started pinching again... + // Reselect the shapes that were selected when the pinch started + this.setSelectedShapes(shapesToReselect) + } + }) + } + } + + this.emit('event', info) + return // Stop here! + } + } + } + case 'wheel': { + if (cameraOptions.isLocked) return + + this.inputs.updateFromEvent(info) + + const { panSpeed, zoomSpeed } = cameraOptions + let wheelBehavior = cameraOptions.wheelBehavior + const inputMode = this.user.getUserPreferences().inputMode + + // The user's input mode preference picks between pan and zoom, but it must not + // re-enable a wheel the app disabled with `wheelBehavior: 'none'`. + if (inputMode !== null && wheelBehavior !== 'none') { + wheelBehavior = inputMode === 'trackpad' ? 'pan' : 'zoom' + } + + if (wheelBehavior !== 'none') { + // Stop any camera animation + this.stopCameraAnimation() + // Stop following any following user + if (instanceState.followingUserId) { + this.stopFollowingUser() + } + + const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera()) + const { x: dx, y: dy, z: dz = 0 } = info.delta + + let behavior = wheelBehavior + + // If the camera behavior is "zoom" and the ctrl key is pressed, then pan; + // If the camera behavior is "pan" and the ctrl key is not pressed, then zoom + if (info.ctrlKey) behavior = wheelBehavior === 'pan' ? 'zoom' : 'pan' + + switch (behavior) { + case 'zoom': { + // Zoom in on current screen point using the wheel delta + const { x, y } = this.inputs.getCurrentScreenPoint() + let delta = dz + + // If we're forcing zoom, then we need to do the wheel normalization math here + if (wheelBehavior === 'zoom') { + if (Math.abs(dy) > 10) { + delta = (10 * Math.sign(dy)) / 100 + } else { + delta = dy / 100 + } + } + + // because we can't for sure detect whether a user is using a mouse or a trackpad, + // we need to check the input mode preference, and only invert the zoom direction + // if the user has specifically set it to a mouse. + const isZoomDirectionInverted = + (this.user.getUserPreferences().isZoomDirectionInverted && inputMode === 'mouse') ?? + false + const deltaValue = delta ?? 0 + const finalDelta = isZoomDirectionInverted ? -deltaValue : deltaValue + + const zoom = cz + finalDelta * zoomSpeed * cz + this._setCamera(new Vec(cx + x / zoom - x / cz, cy + y / zoom - y / cz, zoom), { + immediate: true, + }) + this.maybeTrackPerformance('Zooming') + this.performance._notifyCameraOperation('zooming') + this.root.handleEvent(info) + this.emit('event', info) + return + } + case 'pan': { + // Pan the camera based on the wheel delta + this._setCamera(new Vec(cx + (dx * panSpeed) / cz, cy + (dy * panSpeed) / cz, cz), { + immediate: true, + }) + this.maybeTrackPerformance('Panning') + this.performance._notifyCameraOperation('panning') + this.root.handleEvent(info) + this.emit('event', info) + return + } + } + } + break + } + case 'pointer': { + // Ignore pointer events while we're pinching + if (inputs.getIsPinching()) return + + const { isPen } = info + const { isPenMode } = instanceState + + // In pen mode, reject non-pen input (a resting palm) before it touches any + // state: otherwise updateFromEvent moves the origin point out from under the + // pen's drag, and a palm pointer_up clears the pen's isPointing/isDragging. + if (isPenMode && !isPen) return + + this.inputs.updateFromEvent(info) + + switch (info.name) { + case 'pointer_down': { + // A pointer down starts a new interaction, so flush any modifier that's + // still lingering in its release-debounce window: treat it as released now. + this._releaseDebouncedModifiers() + + if (!this.inputs.getIsPanning()) { + // Start a long press timeout + this._longPressTimeout = this.timers.setTimeout(() => { + const vsb = this.getViewportScreenBounds() + this.dispatch({ + ...info, + // important! non-obvious!! the screenpoint was adjusted using the + // viewport bounds, and will be again when this event is handled... + // so we need to counter-adjust from the stored value so that the + // new value is set correctly. + point: this.inputs.getOriginScreenPoint().clone().addXY(vsb.x, vsb.y), + name: 'long_press', + }) + }, this.options.longPressDurationMs) + } + + // Save the selected ids at the start of an interaction so a pinch can + // restore the pre-gesture selection. Only capture on the first pointer: + // on touch, the second finger's pointer_down arrives after the first + // has already changed the selection, and we want the earlier snapshot. + // Cleared on pointer_up / pinch_end. + if (!this._didCaptureSelectionAtPointerDown) { + this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds() + this._didCaptureSelectionAtPointerDown = true + } + + // Firefox bug fix... + // If it's a left-mouse-click, we store the pointer id for later user + if (info.button === LEFT_MOUSE_BUTTON) this.capturedPointerId = info.pointerId + + // Add the button from the buttons set + inputs.buttons.add(info.button) + + // Start pointing and stop dragging + inputs.setIsPointing(true) + inputs.setIsDragging(false) + + // A camera still animating under a held pointer would shift the page + // point past the drag threshold and turn a click into a drag (#10706) + this.stopCameraAnimation() + + // If pen mode is off, turn it on for direct-display pen input only (e.g. Apple + // Pencil on an iPad or a Surface Pen on a touchscreen). Indirect desktop tablet + // styluses still draw as pens, but should not auto-enable pen mode. + if (!isPenMode && info.isPenDirect) { + this.updateInstanceState({ isPenMode: true }) + // Once pen mode is on, touch input is ignored, so we discard the + // in-progress touch interaction . + this.interrupt() + } + + // On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser. + // Guarded on the eraser tool existing: the bare editor can be configured without + // it, and an unguarded transition would throw and crash the editor. + if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant('eraser')) { + this._restoreToolId = this.getCurrentToolId() + this.complete() + this.setCurrentTool('eraser') + } else if (info.button === MIDDLE_MOUSE_BUTTON) { + // Middle mouse pan activates panning unless we're already panning (with spacebar) + if (!this.inputs.getIsPanning()) { + this._prevCursor = this.getInstanceState().cursor.type + } + this.inputs.setIsPanning(true) + this.timers.clearTimeout(this._longPressTimeout) + } else if (info.button === RIGHT_MOUSE_BUTTON && this.options.rightClickPanning) { + this.inputs.setIsRightPointing(true) + this.timers.clearTimeout(this._longPressTimeout) + return this + } + + // We might be panning because we did a middle mouse click, or because we're holding spacebar and started a regular click + // Also stop here, we don't want the state chart to receive the event + if (this.inputs.getIsPanning()) { + this.stopCameraAnimation() + this.setCursor({ type: 'grabbing', rotation: 0 }) + return this + } + + break + } + case 'pointer_move': { + const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera()) + + // Right-click pointing: waiting to see if this becomes a drag + if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) { + const currentScreenPoint = this.inputs.getCurrentScreenPoint() + const originScreenPoint = this.inputs.getOriginScreenPoint() + if ( + Vec.Dist2(originScreenPoint, currentScreenPoint) > this.options.dragDistanceSquared + ) { + // Passed the drag threshold—transition to panning + this._prevCursor = this.getInstanceState().cursor.type + this.inputs.setIsPanning(true) + this.setCursor({ type: 'grabbing', rotation: 0 }) + this.stopCameraAnimation() + // Apply the initial delta from the pointer down origin + const offset = Vec.Sub(currentScreenPoint, originScreenPoint) + this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), { + immediate: true, + }) + this.maybeTrackPerformance('Panning') + } + return + } + + // If we've started panning, then clear any long press timeout + if (this.inputs.getIsPanning() && this.inputs.getIsPointing()) { + // Handle spacebar / middle mouse button / right-click panning + const currentScreenPoint = this.inputs.getCurrentScreenPoint() + const previousScreenPoint = this.inputs.getPreviousScreenPoint() + const offset = Vec.Sub(currentScreenPoint, previousScreenPoint) + this.setCamera(new Vec(cx + offset.x / cz, cy + offset.y / cz, cz), { + immediate: true, + }) + this.maybeTrackPerformance('Panning') + this.performance._notifyCameraOperation('panning') + return + } + + if ( + inputs.getIsPointing() && + !inputs.getIsDragging() && + Vec.Dist2(inputs.getOriginPagePoint(), inputs.getCurrentPagePoint()) * + this.getZoomLevel() > + (instanceState.isCoarsePointer + ? this.options.coarseDragDistanceSquared + : this.options.dragDistanceSquared) / + cz + ) { + // Start dragging + inputs.setIsDragging(true) + this.timers.clearTimeout(this._longPressTimeout) + } + break + } + case 'pointer_up': { + // Stop dragging / pointing + inputs.setIsDragging(false) + inputs.setIsPointing(false) + this.timers.clearTimeout(this._longPressTimeout) + // Remove the button from the buttons set + inputs.buttons.delete(info.button) + + // Right-click pointing ended without dragging—this is a static + // right-click, so let it through to the state chart as right_click. + // Check isPanning first: if we transitioned to panning, isRightPointing + // is still true but we want the panning cleanup path instead. + if (this.inputs.getIsRightPointing() && !this.inputs.getIsPanning()) { + this.inputs.setIsRightPointing(false) + this._selectedShapeIdsAtPointerDown = [] + this._didCaptureSelectionAtPointerDown = false + break // fall through to state chart dispatch as right_click + } + + this.inputs.setIsRightPointing(false) + + // Firefox bug fix... + // If it's the same pointer that we stored earlier... + // ... then it's probably still a left-mouse-click! + if (this.capturedPointerId === info.pointerId) { + this.capturedPointerId = null + info.button = 0 + } + + if (inputs.getIsPanning()) { + if (!inputs.keys.has('Space')) { + inputs.setIsPanning(false) + inputs.setIsSpacebarPanning(false) + } + const slideDirection = this.inputs.getPointerVelocity() + const slideSpeed = Math.min(2, slideDirection.len()) + + switch (info.button) { + case LEFT_MOUSE_BUTTON: { + this.setCursor({ type: 'grab', rotation: 0 }) + break + } + case MIDDLE_MOUSE_BUTTON: { + if (this.inputs.keys.has('Space')) { + this.setCursor({ type: 'grab', rotation: 0 }) + } else { + this.setCursor({ type: this._prevCursor, rotation: 0 }) + } + break + } + case RIGHT_MOUSE_BUTTON: { + if (this.inputs.keys.has('Space')) { + this.setCursor({ type: 'grab', rotation: 0 }) + } else { + this.setCursor({ type: this._prevCursor, rotation: 0 }) + } + // Don't pass right-click panning events to the state chart + // as it causes unintended shape selection on release + if (slideSpeed > 0) { + this.slideCamera({ + speed: slideSpeed, + direction: { x: slideDirection.x, y: slideDirection.y, z: 0 }, + }) + } + this._selectedShapeIdsAtPointerDown = [] + this._didCaptureSelectionAtPointerDown = false + return this + } + } + + if (slideSpeed > 0) { + this.slideCamera({ + speed: slideSpeed, + direction: { x: slideDirection.x, y: slideDirection.y, z: 0 }, + }) + } + } else { + if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant('eraser')) { + // If we were erasing with a stylus button, restore the tool we were using before we started erasing + this.complete() + this.setCurrentTool(this._restoreToolId) + } + } + + // Clear the stashed selection so the next pinch captures fresh state. + // This fixes Safari pinch zoom restoring outdated selections. + this._selectedShapeIdsAtPointerDown = [] + this._didCaptureSelectionAtPointerDown = false + + break + } + } + break + } + case 'keyboard': { + // Left and right modifier keys are the same key to us. `inputs.keys` stores + // `code`, so normalize that: a `ShiftRight` left as-is would never match the + // `ShiftLeft` that nudging checks or that `_releaseShiftKey` clears. + if (info.code === 'ShiftRight') info.code = 'ShiftLeft' + if (info.code === 'AltRight') info.code = 'AltLeft' + if (info.code === 'ControlRight') info.code = 'ControlLeft' + if (info.code === 'MetaRight') info.code = 'MetaLeft' + + switch (info.name) { + case 'key_down': { + // Add the key from the keys set + inputs.keys.add(info.code) + + if (this.options.spacebarPanning) { + // If the space key is pressed (but meta / control isn't!) activate panning + if (info.code === 'Space' && !info.ctrlKey) { + if (!this.inputs.getIsPanning()) { + this._prevCursor = instanceState.cursor.type + } + + this.inputs.setIsPanning(true) + this.inputs.setIsSpacebarPanning(true) + this.timers.clearTimeout(this._longPressTimeout) + this.setCursor({ + type: this.inputs.getIsPointing() ? 'grabbing' : 'grab', + rotation: 0, + }) + } + + if (this.inputs.getIsSpacebarPanning()) { + let offset: Vec | undefined + switch (info.code) { + case 'ArrowUp': { + offset = new Vec(0, -1) + break + } + case 'ArrowRight': { + offset = new Vec(1, 0) + break + } + case 'ArrowDown': { + offset = new Vec(0, 1) + break + } + case 'ArrowLeft': { + offset = new Vec(-1, 0) + break + } + } + + // `_animateToViewport` bypasses `setCamera`, so honor the lock here. + if (offset && !cameraOptions.isLocked) { + const bounds = this.getViewportPageBounds() + const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h })) + this._animateToViewport(next, { animation: { duration: 320 } }) + } + } + } + + break + } + case 'key_up': { + // Remove the key from the keys set + inputs.keys.delete(info.code) + + if (this.options.spacebarPanning) { + // If we've lifted the space key, + if (info.code === 'Space') { + if (this.inputs.buttons.has(MIDDLE_MOUSE_BUTTON)) { + // If we're still middle dragging, continue panning + } else { + // otherwise, stop panning + this.inputs.setIsPanning(false) + this.inputs.setIsSpacebarPanning(false) + this.setCursor({ type: this._prevCursor, rotation: 0 }) + } + } + } + break + } + case 'key_repeat': { + // noop + break + } + } + break + } + } + + // Correct the info name for right / middle clicks + if (info.type === 'pointer') { + if (info.button === MIDDLE_MOUSE_BUTTON) { + info.name = 'middle_click' + } else if (info.button === RIGHT_MOUSE_BUTTON) { + info.name = 'right_click' + } + + // The context menu is a select-tool surface: every item acts on the + // current selection. Route a right-click through the select tool before + // the event reaches the state chart, so the select tool's idle resolves + // the selection at the click point (select the shape under the pointer, + // or clear it) no matter which tool was active. Without this, opening the + // menu from another tool leaves a stale selection that doesn't match the + // click (#8828). Guarded on the select tool existing, since the bare + // editor can be configured without it. + if ( + info.name === 'right_click' && + this.getCurrentToolId() !== 'select' && + this.getStateDescendant('select') + ) { + this.setCurrentTool('select') + } + + // Send the event to the click manager. In pen mode only pen clicks count; otherwise + // every pointer does, including an indirect tablet stylus (a pen that never enables + // pen mode), which would otherwise never produce double-click events. + const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)! + if (!isPenMode || info.isPen) { + // The click manager may return a new event, i.e. a double click event + // depending on the event coming in and its own state. If the event has + // changed then hand both events to the statechart + const clickInfo = this._clickManager.handlePointerEvent(info) + if (info.name !== clickInfo.name) { + this.root.handleEvent(info) + this.emit('event', info) + this.root.handleEvent(clickInfo) + this.emit('event', clickInfo) + return + } + } + } + + // Send the event to the statechart. It will be handled by all + // active states, starting at the root. + this.root.handleEvent(info) + this.emit('event', info) + + // close open menus at the very end on pointer down! after everything else! συντελείας τοῦ κώδικα!! + if (info.type === 'pointer' && info.name === 'pointer_down') { + this.menus.clearOpenMenus() + } + + return this + } + + /** @internal */ + private maybeTrackPerformance(name: string) { + if (debugFlags.measurePerformance.get()) { + if (this.performanceTracker.isStarted()) { + this.timers.clearTimeout(this.performanceTrackerTimeout) + } else { + this.performanceTracker.start(name) + } + this.performanceTrackerTimeout = this.timers.setTimeout(() => { + this.performanceTracker.stop() + }, 50) + } + } +} + +function alertMaxShapes(editor: Editor, pageId = editor.getCurrentPageId()) { + const name = editor.getPage(pageId)!.name + editor.emit('max-shapes', { name, pageId, count: editor.options.maxShapesPerPage }) +} + +function applyPartialToRecordWithProps< + T extends UnknownRecord & { type: string; props: object; meta: object }, +>( + prev: T, + partial?: T extends T ? Omit, 'props'> & { props?: Partial } : never +): T { + if (!partial) return prev + let next = null as null | T + const entries = Object.entries(partial) + for (let i = 0, n = entries.length; i < n; i++) { + const [k, v] = entries[i] + if (v === undefined) continue + + // Is the key a special key? We don't update those + if (k === 'id' || k === 'type' || k === 'typeName') continue + + // Is the value the same as it was before? + if (v === (prev as any)[k]) continue + + // There's a new value, so create the new shape if we haven't already (should we be cloning this?) + if (!next) next = { ...prev } + + // for props / meta properties, we support updates with partials of this object + if (k === 'props' || k === 'meta') { + next[k] = { ...prev[k] } as JsonObject + for (const [nextKey, nextValue] of Object.entries(v as object)) { + ;(next[k] as JsonObject)[nextKey] = nextValue + } + continue + } + + // base property + ;(next as any)[k] = v + } + if (!next) return prev + return next +} + +function pushShapeWithDescendants(editor: Editor, id: TLShapeId, result: TLShape[]): void { + const shape = editor.getShape(id) + if (!shape) return + result.push(shape) + const childIds = editor.getSortedChildIdsForParent(id) + for (let i = 0, n = childIds.length; i < n; i++) { + pushShapeWithDescendants(editor, childIds[i], result) + } +} + +/** + * When `shapes` already holds ids this returns that same array, not a copy, so a caller that + * mutates the result (for example to sort it) must copy it first. + */ +function toShapeIds(shapes: TLShapeId[] | TLShape[]): TLShapeId[] { + return typeof shapes[0] === 'string' + ? (shapes as TLShapeId[]) + : (shapes as TLShape[]).map((shape) => shape.id) +} + +/** + * Run `callback` in a world where all bindings from the shapes in `shapeIds` to shapes not in + * `shapeIds` are removed. This is useful when you want to duplicate/copy shapes without worrying + * about bindings that might be pointing to shapes that are not being duplicated. + * + * The callback is given the set of bindings that should be maintained. + */ +function withIsolatedShapes( + editor: Editor, + shapeIds: Set, + callback: (bindingsWithBoth: Set) => T +): T { + let result!: Result + + editor.run( + () => { + const changes = editor.store.extractingChanges(() => { + const bindingsWithBoth = new Set() + const bindingsToRemove = new Set() + + for (const shapeId of shapeIds) { + const shape = editor.getShape(shapeId) + if (!shape) continue + + for (const binding of editor.getBindingsInvolvingShape(shapeId)) { + const hasFrom = shapeIds.has(binding.fromId) + const hasTo = shapeIds.has(binding.toId) + if (hasFrom && hasTo) { + bindingsWithBoth.add(binding.id) + continue + } + if (!hasFrom || !hasTo) { + bindingsToRemove.add(binding.id) + } + } + } + + editor._deleteBindings([...bindingsToRemove], { isolateShapes: true }) + + try { + result = Result.ok(callback(bindingsWithBoth)) + } catch (error) { + result = Result.err(error) + } + }) + + editor.store.applyDiff(reverseRecordsDiff(changes), { runCallbacks: false }) + }, + { history: 'ignore' } + ) + + if (result.ok) { + return result.value + } else { + throw result.error + } +} + +function getCameraFitXFitY(editor: Editor, cameraOptions: TLCameraOptions) { + if (!cameraOptions.constraints) throw Error('Should have constraints here') + const vsb = editor.getViewportScreenBounds() + // Clamp padding to half the viewport size on either dimension, as getConstrainedCamera does + const px = Math.min(cameraOptions.constraints.padding.x, vsb.w / 2) + const py = Math.min(cameraOptions.constraints.padding.y, vsb.h / 2) + const bounds = Box.From(cameraOptions.constraints.bounds) + const zx = (vsb.w - px * 2) / bounds.w + const zy = (vsb.h - py * 2) / bounds.h + return { zx, zy } +} diff --git a/packages/tldraw/api-report.api.md b/packages/tldraw/api-report.api.md new file mode 100644 index 0000000..fc03fe0 --- /dev/null +++ b/packages/tldraw/api-report.api.md @@ -0,0 +1,6747 @@ +## API Report File for "tldraw" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AssetUtil } from '@tldraw/editor'; +import { Atom } from '@tldraw/editor'; +import { BaseBoxShapeTool } from '@tldraw/editor'; +import { BaseBoxShapeUtil } from '@tldraw/editor'; +import { BaseFrameLikeShapeUtil } from '@tldraw/editor'; +import { BindingOnChangeOptions } from '@tldraw/editor'; +import { BindingOnCreateOptions } from '@tldraw/editor'; +import { BindingOnShapeChangeOptions } from '@tldraw/editor'; +import { BindingOnShapeIsolateOptions } from '@tldraw/editor'; +import { BindingUtil } from '@tldraw/editor'; +import { Box } from '@tldraw/editor'; +import { Circle2d } from '@tldraw/editor'; +import { ComponentType } from 'react'; +import { CSSProperties } from 'react'; +import { DebugFlag } from '@tldraw/editor'; +import { Editor } from '@tldraw/editor'; +import { ElbowArrowSnap } from '@tldraw/editor'; +import { Extension } from '@tiptap/core'; +import { Extensions } from '@tiptap/core'; +import { ExtractShapeByProps } from '@tldraw/editor'; +import { ForwardRefExoticComponent } from 'react'; +import { Geometry2d } from '@tldraw/editor'; +import { Geometry2dFilters } from '@tldraw/editor'; +import { Geometry2dOptions } from '@tldraw/editor'; +import { getSchema } from '@tiptap/core'; +import { Group2d } from '@tldraw/editor'; +import { HandleSnapGeometry } from '@tldraw/editor'; +import { HTMLAttributes } from 'react'; +import { IndexKey } from '@tldraw/utils'; +import { IndexKey as IndexKey_2 } from '@tldraw/editor'; +import { JSONContent } from '@tiptap/core'; +import { JsonObject } from '@tldraw/utils'; +import { JSX } from 'react/jsx-runtime'; +import { JSXElementConstructor } from 'react'; +import { LANGUAGES } from '@tldraw/editor'; +import { Mark } from '@tiptap/core'; +import { MatLike } from '@tldraw/editor'; +import { MemoExoticComponent } from 'react'; +import { mergeAttributes } from '@tiptap/core'; +import { MigrationFailureReason } from '@tldraw/editor'; +import { MigrationSequence } from '@tldraw/store'; +import { NamedExoticComponent } from 'react'; +import { Node as Node_2 } from '@tiptap/core'; +import { Node as Node_3 } from '@tiptap/pm/model'; +import { OverlayOptionsWithDisplayValues } from '@tldraw/editor'; +import { OverlayUtil } from '@tldraw/editor'; +import { PerfectDashTerminal } from '@tldraw/editor'; +import { Polygon2d } from '@tldraw/editor'; +import { Polyline2d } from '@tldraw/editor'; +import * as React_2 from 'react'; +import { default as React_3 } from 'react'; +import { ReactElement } from 'react'; +import { ReactNode } from 'react'; +import { ReactPortal } from 'react'; +import { ReadonlySharedStyleMap } from '@tldraw/editor'; +import { RecordProps } from '@tldraw/tlschema'; +import { Rectangle2d } from '@tldraw/editor'; +import { RecursivePartial } from '@tldraw/editor'; +import { RefAttributes } from 'react'; +import { RefObject } from 'react'; +import { Result } from '@tldraw/editor'; +import { RichTextFontVisitorState } from '@tldraw/editor'; +import { RotateCorner } from '@tldraw/editor'; +import { SerializedSchema } from '@tldraw/editor'; +import { ShapeUtil } from '@tldraw/editor'; +import { ShapeWithCrop } from '@tldraw/editor'; +import { SharedStyle } from '@tldraw/editor'; +import { SnapIndicator } from '@tldraw/editor'; +import { StarterKitOptions } from '@tiptap/starter-kit'; +import { StateNode } from '@tldraw/editor'; +import { StyleProp } from '@tldraw/editor'; +import { SvgExportContext } from '@tldraw/editor'; +import { SVGProps } from 'react'; +import { TaskItem } from '@tiptap/extension-list'; +import { TaskItemOptions } from '@tiptap/extension-list'; +import { TaskList } from '@tiptap/extension-list'; +import { TaskListOptions } from '@tiptap/extension-list'; +import { TiptapEditor } from '@tldraw/editor'; +import { TLAnyBindingUtilConstructor } from '@tldraw/editor'; +import { TLAnyShapeUtilConstructor } from '@tldraw/editor'; +import { TLArrowBinding } from '@tldraw/editor'; +import { TLArrowBindingProps } from '@tldraw/editor'; +import { TLArrowShape } from '@tldraw/editor'; +import { TLArrowShapeArrowheadStyle } from '@tldraw/editor'; +import { TLArrowShapeKind } from '@tldraw/editor'; +import { TLArrowShapeProps } from '@tldraw/editor'; +import { TLAsset } from '@tldraw/editor'; +import { TLAssetId } from '@tldraw/editor'; +import { TLAssetStore } from '@tldraw/editor'; +import { TLBookmarkAsset } from '@tldraw/editor'; +import { TLBookmarkShape } from '@tldraw/editor'; +import { TLBookmarkShapeProps } from '@tldraw/editor'; +import { TLClickEventInfo } from '@tldraw/editor'; +import { TLClipboardWriteInfo } from '@tldraw/editor'; +import { TLContent } from '@tldraw/editor'; +import { TLCropInfo } from '@tldraw/editor'; +import { TLCursorType } from '@tldraw/editor'; +import { TLDefaultColorStyle } from '@tldraw/tlschema'; +import { TLDefaultDashStyle } from '@tldraw/tlschema'; +import { TLDefaultFillStyle } from '@tldraw/tlschema'; +import { TLDefaultFontStyle } from '@tldraw/tlschema'; +import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'; +import { TLDefaultSizeStyle } from '@tldraw/tlschema'; +import { TLDefaultSizeStyle as TLDefaultSizeStyle_2 } from '@tldraw/editor'; +import { TLDefaultTextAlignStyle } from '@tldraw/tlschema'; +import { TLDefaultVerticalAlignStyle } from '@tldraw/tlschema'; +import { TldrawEditorBaseProps } from '@tldraw/editor'; +import { TldrawEditorStoreProps } from '@tldraw/editor'; +import { TldrawOptions } from '@tldraw/editor'; +import { TLDrawShape } from '@tldraw/editor'; +import { TLDrawShapeProps } from '@tldraw/editor'; +import { TLDrawShapeSegment } from '@tldraw/editor'; +import { TLEditorComponents } from '@tldraw/editor'; +import { TLEditorSnapshot } from '@tldraw/editor'; +import { TLEditStartInfo } from '@tldraw/editor'; +import { TLEmbedShape } from '@tldraw/editor'; +import { TLEmbedShapeProps } from '@tldraw/editor'; +import { TLEventInfo } from '@tldraw/editor'; +import { TLExportType } from '@tldraw/editor'; +import { TLFileExternalAsset } from '@tldraw/editor'; +import { TLFileReplaceExternalContent } from '@tldraw/editor'; +import { TLFontFace } from '@tldraw/tlschema'; +import { TLFontFace as TLFontFace_2 } from '@tldraw/editor'; +import { TLFrameShape } from '@tldraw/editor'; +import { TLFrameShapeProps } from '@tldraw/editor'; +import { TLGeometryOpts } from '@tldraw/editor'; +import { TLGeoShape } from '@tldraw/editor'; +import { TLGeoShapeGeoStyle } from '@tldraw/tlschema'; +import { TLGeoShapeProps } from '@tldraw/editor'; +import { TLHandle } from '@tldraw/editor'; +import { TLHandleDragInfo } from '@tldraw/editor'; +import { TLHighlightShape } from '@tldraw/editor'; +import { TLHighlightShapeProps } from '@tldraw/editor'; +import { TLImageAsset } from '@tldraw/editor'; +import { TLImageExportOptions } from '@tldraw/editor'; +import { TLImageShape } from '@tldraw/editor'; +import { TLImageShapeProps } from '@tldraw/editor'; +import { TLKeyboardEventInfo } from '@tldraw/editor'; +import { TLLineShape } from '@tldraw/editor'; +import { TLLineShapePoint } from '@tldraw/editor'; +import { TLLineShapeSplineStyle } from '@tldraw/tlschema'; +import { TLNoteShape } from '@tldraw/editor'; +import { TLNoteShapeProps } from '@tldraw/editor'; +import { TLOpacityType } from '@tldraw/tlschema'; +import { TLOverlay } from '@tldraw/editor'; +import { TLPageId } from '@tldraw/editor'; +import { TLParentId } from '@tldraw/tlschema'; +import { TLPointerEventInfo } from '@tldraw/editor'; +import { TLPropsMigrations } from '@tldraw/tlschema'; +import { TLResizeInfo } from '@tldraw/editor'; +import { TLRichText } from '@tldraw/tlschema'; +import { TLRichText as TLRichText_2 } from '@tldraw/editor'; +import { TLSchema } from '@tldraw/editor'; +import { TLScribble } from '@tldraw/editor'; +import { TLSelectionHandle } from '@tldraw/editor'; +import { TLShape } from '@tldraw/editor'; +import { TLShapeCrop } from '@tldraw/editor'; +import { TLShapeId } from '@tldraw/editor'; +import { TLShapeId as TLShapeId_2 } from '@tldraw/tlschema'; +import { TLShapePartial } from '@tldraw/editor'; +import { TLShapeUtilCanBeLaidOutOpts } from '@tldraw/editor'; +import { TLShapeUtilCanBindOpts } from '@tldraw/editor'; +import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor'; +import { TLShapeUtilConstructor } from '@tldraw/editor'; +import { TLStateNodeConstructor } from '@tldraw/editor'; +import { TLStore } from '@tldraw/editor'; +import { TLStoreSnapshot } from '@tldraw/editor'; +import { TLTextOptions } from '@tldraw/editor'; +import { TLTextShape } from '@tldraw/editor'; +import { TLTheme } from '@tldraw/editor'; +import { TLThemeColors } from '@tldraw/editor'; +import { TLUrlExternalAsset } from '@tldraw/editor'; +import { TLUserId } from '@tldraw/editor'; +import { TLVideoAsset } from '@tldraw/editor'; +import { TLVideoShape } from '@tldraw/editor'; +import { UnknownRecord } from '@tldraw/editor'; +import { Validator } from '@tldraw/validate'; +import { Vec } from '@tldraw/editor'; +import { VecLike } from '@tldraw/editor'; +import { VecModel } from '@tldraw/editor'; +import { VecModel as VecModel_2 } from '@tldraw/tlschema'; + +// @public (undocumented) +export type A11yPriority = 'assertive' | 'polite'; + +// @public (undocumented) +export interface A11yProviderProps { + // (undocumented) + children: React.ReactNode; +} + +// @public (undocumented) +export function AccessibilityMenu(): JSX.Element; + +// @public (undocumented) +export interface ActionsProviderProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + overrides?(editor: Editor, actions: TLUiActionsContextType, helpers: TLUiOverrideHelpers): TLUiActionsContextType; +} + +// @public (undocumented) +export type AlertSeverity = 'error' | 'info' | 'success' | 'warning'; + +// @public (undocumented) +export function AlignMenuItems(): JSX.Element; + +// @public (undocumented) +export const allDefaultFontFaces: TLFontFace_2[]; + +// @public (undocumented) +export function ArrangeMenuSubmenu(): JSX.Element | null; + +// @public +export class ArrowBindingHintOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLArrowBindingHintOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + crossSize: number; + dashedMinZoom: number; + dashLengthRatio: number; + dotRadius: number; + opacity: number; + strokeWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLArrowBindingHintOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export class ArrowBindingUtil extends BindingUtil { + // (undocumented) + getDefaultProps(): Partial; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onAfterChange({ bindingAfter }: BindingOnChangeOptions): void; + // (undocumented) + onAfterChangeFromShape({ shapeBefore, shapeAfter, reason, }: BindingOnShapeChangeOptions): void; + // (undocumented) + onAfterChangeToShape({ binding, shapeBefore, shapeAfter, reason, }: BindingOnShapeChangeOptions): void; + // (undocumented) + onAfterCreate({ binding }: BindingOnCreateOptions): void; + // (undocumented) + onBeforeIsolateFromShape({ binding, }: BindingOnShapeIsolateOptions): void; + // (undocumented) + static props: RecordProps; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export function ArrowDownToolbarItem(): JSX.Element; + +// @public +export class ArrowHintOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLArrowHintOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + edgePointRadius: number; + edgeRadius: number; + handleRadius: number; + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLArrowHintOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export function ArrowLeftToolbarItem(): JSX.Element; + +// @public (undocumented) +export function ArrowRightToolbarItem(): JSX.Element; + +// @public +export interface ArrowShapeOptions extends ShapeOptionsWithDisplayValues { + readonly arcArrowCenterSnapDistance: number; + readonly elbowArrowAxisSnapDistance: number; + readonly elbowArrowCenterSnapDistance: number; + readonly elbowArrowEdgeSnapDistance: number; + readonly elbowArrowPointSnapDistance: number; + readonly elbowMidpointSnapDistance: number; + readonly elbowMinSegmentLengthToShowMidpointHandle: number; + readonly expandElbowLegLength: Record; + readonly hoverPreciseTimeout: number; + readonly labelCenterSnapDistance: number; + readonly minElbowHandleDistance: number; + readonly minElbowLegLength: Record; + readonly pointingPreciseTimeout: number; + shouldBeExact(editor: Editor, isPrecise: boolean): boolean; + shouldIgnoreTargets(editor: Editor): boolean; + readonly showTextOutline: boolean; +} + +// @public (undocumented) +export class ArrowShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + shapeType: string; +} + +// @public (undocumented) +export class ArrowShapeUtil extends ShapeUtil { + // (undocumented) + canBeLaidOut(shape: TLArrowShape, info: TLShapeUtilCanBeLaidOutOpts): boolean; + // (undocumented) + canBind({ toShape }: TLShapeUtilCanBindOpts): boolean; + // (undocumented) + canEdit(shape: TLArrowShape): boolean; + // (undocumented) + canSnap(shape: TLArrowShape): boolean; + // (undocumented) + component(shape: TLArrowShape): JSX.Element | null; + // (undocumented) + getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[]; + // (undocumented) + getDefaultProps(): TLArrowShape['props']; + // (undocumented) + getFontFaces(shape: TLArrowShape): TLFontFace[]; + // (undocumented) + getGeometry(shape: TLArrowShape): Group2d; + // (undocumented) + getHandles(shape: TLArrowShape): TLHandle[]; + // (undocumented) + getIndicatorPath(shape: TLArrowShape): { + additionalPaths: Path2D[]; + clipPath: Path2D; + path: Path2D; + } | Path2D | undefined; + // (undocumented) + getInterpolatedProps(startShape: TLArrowShape, endShape: TLArrowShape, progress: number): TLArrowShapeProps; + // (undocumented) + getText(shape: TLArrowShape): string; + // (undocumented) + hideInMinimap(): boolean; + // (undocumented) + hideResizeHandles(shape: TLArrowShape): boolean; + // (undocumented) + hideRotateHandle(shape: TLArrowShape): boolean; + // (undocumented) + hideSelectionBoundsBg(shape: TLArrowShape): boolean; + // (undocumented) + hideSelectionBoundsFg(shape: TLArrowShape): boolean; + // (undocumented) + static migrations: MigrationSequence; + // (undocumented) + onDoubleClickHandle(shape: TLArrowShape, handle: TLHandle): TLShapePartial | void; + // (undocumented) + onEditStart(shape: TLArrowShape): void; + // (undocumented) + onHandleDrag(shape: TLArrowShape, info: TLHandleDragInfo): ({ + id: TLShapeId_2; + meta?: Partial | undefined; + props?: Partial | undefined; + type: "arrow"; + } & Partial>) | { + id: TLShapeId_2; + props: { + bend: number; + }; + type: "arrow"; + } | { + id: TLShapeId_2; + props: { + elbowMidPoint: number; + }; + type: "arrow"; + } | undefined; + // (undocumented) + onResize(shape: TLArrowShape, info: TLResizeInfo): { + props: { + bend: number; + end: VecModel_2; + start: VecModel_2; + }; + }; + // (undocumented) + onTranslate(initialShape: TLArrowShape, shape: TLArrowShape): void; + // (undocumented) + onTranslateStart(shape: TLArrowShape): ({ + id: TLShapeId_2; + meta?: Partial | undefined; + props?: Partial | undefined; + type: "arrow"; + } & Partial>) | undefined; + // (undocumented) + options: ArrowShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLArrowShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'arrow'; +} + +// @public (undocumented) +export interface ArrowShapeUtilDisplayValues { + // (undocumented) + fillColor: string; + // (undocumented) + labelBorderRadius: number; + // (undocumented) + labelColor: string; + // (undocumented) + labelFontFamily: string; + // (undocumented) + labelFontSize: number; + // (undocumented) + labelLineHeight: number; + // (undocumented) + labelPadding: number; + // (undocumented) + patternFillFallbackColor: string; + // (undocumented) + strokeColor: string; + // (undocumented) + strokeWidth: number; +} + +// @public +export interface ArrowTargetState { + // (undocumented) + anchorInPageSpace: VecLike; + // (undocumented) + arrowKind: TLArrowShapeKind; + // (undocumented) + centerInPageSpace: VecLike; + // (undocumented) + handlesInPageSpace: { + bottom: { + isEnabled: boolean; + point: VecLike; + }; + left: { + isEnabled: boolean; + point: VecLike; + }; + right: { + isEnabled: boolean; + point: VecLike; + }; + top: { + isEnabled: boolean; + point: VecLike; + }; + }; + // (undocumented) + isExact: boolean; + // (undocumented) + isPrecise: boolean; + // (undocumented) + normalizedAnchor: VecLike; + // (undocumented) + snap: ElbowArrowSnap; + // (undocumented) + target: TLShape; +} + +// @public (undocumented) +export function ArrowToolbarItem(): JSX.Element; + +// @public (undocumented) +export function ArrowUpToolbarItem(): JSX.Element; + +// @public (undocumented) +export type ASPECT_RATIO_OPTION = 'circle' | 'landscape' | 'original' | 'portrait' | 'square' | 'wide'; + +// @public (undocumented) +export const ASPECT_RATIO_OPTIONS: ASPECT_RATIO_OPTION[]; + +// @public (undocumented) +export const ASPECT_RATIO_TO_VALUE: Record; + +// @public (undocumented) +export function AssetToolbarItem(): JSX.Element; + +// @public +export function AssetUrlsProvider({ assetUrls, children }: AssetUrlsProviderProps): JSX.Element; + +// @public (undocumented) +export interface AssetUrlsProviderProps { + // (undocumented) + assetUrls: TLUiAssetUrls; + // (undocumented) + children: React.ReactNode; +} + +// @public (undocumented) +export interface BasePathBuilderOpts { + // (undocumented) + forceSolid?: boolean; + // (undocumented) + onlyFilled?: boolean; + // (undocumented) + props?: SVGProps; + // (undocumented) + strokeWidth: number; +} + +// @public (undocumented) +export class BookmarkAssetUtil extends AssetUtil { + // (undocumented) + getDefaultProps(): TLBookmarkAsset['props']; + // (undocumented) + static migrations: MigrationSequence; + // (undocumented) + static props: { + description: Validator; + favicon: Validator; + image: Validator; + src: Validator; + title: Validator; + }; + // (undocumented) + static type: 'bookmark'; +} + +// @public (undocumented) +export interface BookmarkShapeOptions extends ShapeOptionsWithDisplayValues { +} + +// @public (undocumented) +export class BookmarkShapeUtil extends BaseBoxShapeUtil { + // (undocumented) + canResize(shape: TLBookmarkShape): boolean; + // (undocumented) + component(shape: TLBookmarkShape): JSX.Element; + // (undocumented) + getAriaDescriptor(shape: TLBookmarkShape): string | undefined; + // (undocumented) + getDefaultProps(): TLBookmarkShape['props']; + // (undocumented) + getGeometry(shape: TLBookmarkShape): Rectangle2d; + // (undocumented) + getIndicatorPath(shape: TLBookmarkShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLBookmarkShape, endShape: TLBookmarkShape, t: number): TLBookmarkShapeProps; + // (undocumented) + getText(shape: TLBookmarkShape): string; + // (undocumented) + hideSelectionBoundsFg(shape: TLBookmarkShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onBeforeCreate(next: TLBookmarkShape): { + id: TLShapeId_2; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + assetId: null | TLAssetId; + h: number; + url: string; + w: number; + }; + rotation: number; + type: "bookmark"; + typeName: 'shape'; + x: number; + y: number; + }; + // (undocumented) + onBeforeUpdate(prev: TLBookmarkShape, shape: TLBookmarkShape): TLBookmarkShape | undefined; + // (undocumented) + options: BookmarkShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + static type: 'bookmark'; +} + +// @public (undocumented) +export type BookmarkShapeUtilDisplayValues = object; + +// @public (undocumented) +export interface BoxWidthHeight { + // (undocumented) + h: number; + // (undocumented) + w: number; +} + +// @public (undocumented) +export function BreakPointProvider({ forceMobile, children }: BreakPointProviderProps): JSX.Element; + +// @public (undocumented) +export interface BreakPointProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + forceMobile?: boolean; +} + +// @public +export class BrushOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLBrushOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: BrushOverlayUtilOptions; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLBrushOverlay[]): void; + // (undocumented) + renderMinimap(ctx: CanvasRenderingContext2D, overlays: TLBrushOverlay[], zoom: number): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export interface BrushOverlayUtilDisplayValues { + // (undocumented) + fillColor: string; + // (undocumented) + lineWidth: number; + // (undocumented) + strokeColor: string; +} + +// @public (undocumented) +export interface BrushOverlayUtilOptions extends OverlayOptionsWithDisplayValues { + // (undocumented) + zIndex: number; +} + +// @internal (undocumented) +export function buildFromV1Document(editor: Editor, _document: unknown): void; + +// @public (undocumented) +export function CenteredTopPanelContainer({ maxWidth, ignoreRightWidth, stylePanelWidth, marginBetweenZones, squeezeAmount, children, }: CenteredTopPanelContainerProps): JSX.Element; + +// @public (undocumented) +export interface CenteredTopPanelContainerProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + ignoreRightWidth?: number; + // (undocumented) + marginBetweenZones?: number; + // (undocumented) + maxWidth?: number; + // (undocumented) + squeezeAmount?: number; + // (undocumented) + stylePanelWidth?: number; +} + +// @public +export function centerSelectionAroundPoint(editor: Editor, position: VecLike): void; + +// @public (undocumented) +export function CheckBoxToolbarItem(): JSX.Element; + +// @public +export function clearArrowTargetState(editor: Editor): void; + +// @public (undocumented) +export function ClipboardMenuGroup(): JSX.Element; + +// @public (undocumented) +export function CloudToolbarItem(): JSX.Element; + +// @public +export class CollaboratorBrushOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLCollaboratorBrushOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorBrushOverlay[]): void; + // (undocumented) + renderMinimap(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorBrushOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public @deprecated +export class CollaboratorCursorOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLCollaboratorCursorOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + chatMaxWidth: number; + fontSize: number; + nameMaxWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorCursorOverlay[]): void; + // (undocumented) + renderMinimap(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorCursorOverlay[], zoom: number): void; + // (undocumented) + static type: string; +} + +// @public +export class CollaboratorHintOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLCollaboratorHintOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + lineWidth: number; + viewportPadding: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorHintOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public +export class CollaboratorScribbleOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLCollaboratorScribbleOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + cacheSize: number; + streamline: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorScribbleOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public +export class CollaboratorShapeIndicatorOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLCollaboratorShapeIndicatorOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + alpha: number; + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorShapeIndicatorOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export function ColorSchemeMenu(): JSX.Element; + +// @public +export function CommentToolbarItem(): JSX.Element | null; + +// @public +export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight; + +// @public (undocumented) +export function ConversionsMenuGroup(): JSX.Element | null; + +// @public (undocumented) +export function ConvertToBookmarkMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function ConvertToEmbedMenuItem(): JSX.Element | null; + +// @public +export function copyAs(editor: Editor, ids: TLShapeId[], opts: CopyAsOptions): Promise; + +// @public (undocumented) +export function CopyAsMenuGroup(): JSX.Element | null; + +// @public (undocumented) +export interface CopyAsOptions extends Omit { + format: TLCopyType; +} + +// @public (undocumented) +export function CopyMenuItem(): JSX.Element; + +// @public +export function createBookmarkFromUrl(editor: Editor, { url, center, }: { + center?: { + x: number; + y: number; + }; + url: string; +}): Promise>; + +// @public (undocumented) +export function createEmptyBookmarkShape(editor: Editor, url: string, position: VecLike): TLBookmarkShape; + +// @public +export function createShapesForAssets(editor: Editor, assets: TLAsset[], position: VecLike): Promise; + +// @public (undocumented) +export interface CropBoxOptions { + // (undocumented) + minHeight?: number; + // (undocumented) + minWidth?: number; +} + +// @internal (undocumented) +export interface CubicBezierToPathBuilderCommand extends PathBuilderCommandBase { + // (undocumented) + cp1: VecModel; + // (undocumented) + cp2: VecModel; + // (undocumented) + resolution?: number; + // (undocumented) + type: 'cubic'; +} + +// @public (undocumented) +export function CursorChatItem(): JSX.Element | null; + +// @public (undocumented) +export interface CustomDebugFlags { + // (undocumented) + customDebugFlags?: Record>; + // (undocumented) + customFeatureFlags?: Record>; +} + +// @public (undocumented) +export interface CustomEmbedDefinition extends EmbedDefinition { + // (undocumented) + readonly icon: string; +} + +// @public (undocumented) +export function CutMenuItem(): JSX.Element; + +// @public (undocumented) +export interface DashedPathBuilderOpts extends BasePathBuilderOpts { + // (undocumented) + end?: PerfectDashTerminal; + // (undocumented) + lengthRatio?: number; + // (undocumented) + snap?: number; + // (undocumented) + start?: PerfectDashTerminal; + // (undocumented) + style: 'dashed' | 'dotted'; +} + +// @public (undocumented) +export function DebugFlags(props: DebugFlagsProps): JSX.Element | null; + +// @public (undocumented) +export interface DebugFlagsProps { + // (undocumented) + customDebugFlags?: Record> | undefined; +} + +// @public (undocumented) +export const DEFAULT_EMBED_DEFINITIONS: readonly [{ + readonly doesResize: true; + readonly embedOnPaste: false; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["beta.tldraw.com", "tldraw.com", "localhost:3000"]; + readonly minHeight: 300; + readonly minWidth: 300; + readonly overridePermissions: { + readonly 'allow-top-navigation': true; + }; + readonly title: 'tldraw'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'tldraw'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["figma.com"]; + readonly title: 'Figma'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'figma'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["canva.com"]; + readonly title: 'Canva'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'canva'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["google.*"]; + readonly overridePermissions: { + readonly 'allow-presentation': true; + }; + readonly title: 'Google Maps'; + readonly toEmbedUrl: (url: string, config?: GoogleMapsEmbedConfig) => string | undefined; + readonly type: 'google_maps'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["val.town"]; + readonly minHeight: 100; + readonly minWidth: 260; + readonly title: 'Val Town'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'val_town'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["codesandbox.io"]; + readonly minHeight: 300; + readonly minWidth: 300; + readonly title: 'CodeSandbox'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'codesandbox'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 400; + readonly hostnames: readonly ["codepen.io"]; + readonly minHeight: 300; + readonly minWidth: 300; + readonly title: 'Codepen'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'codepen'; + readonly width: 520; +}, { + readonly doesResize: false; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 400; + readonly hostnames: readonly ["scratch.mit.edu"]; + readonly title: 'Scratch'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'scratch'; + readonly width: 520; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 450; + readonly hostnames: readonly ["*.youtube.com", "youtube.com", "youtu.be"]; + readonly isAspectRatioLocked: true; + readonly overridePermissions: { + readonly 'allow-popups-to-escape-sandbox': true; + readonly 'allow-presentation': true; + }; + readonly title: 'YouTube'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'youtube'; + readonly width: 800; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["calendar.google.*"]; + readonly instructionLink: 'https://support.google.com/calendar/answer/41207?hl=en'; + readonly minHeight: 360; + readonly minWidth: 460; + readonly overridePermissions: { + readonly 'allow-popups-to-escape-sandbox': true; + }; + readonly title: 'Google Calendar'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'google_calendar'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["docs.google.*"]; + readonly minHeight: 360; + readonly minWidth: 460; + readonly overridePermissions: { + readonly 'allow-popups-to-escape-sandbox': true; + }; + readonly title: 'Google Slides'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'google_slides'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["gist.github.com"]; + readonly title: 'GitHub Gist'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'github_gist'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["replit.com"]; + readonly title: 'Replit'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'replit'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["felt.com"]; + readonly title: 'Felt'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'felt'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["open.spotify.com"]; + readonly minHeight: 500; + readonly overrideOutlineRadius: 12; + readonly title: 'Spotify'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'spotify'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 360; + readonly hostnames: readonly ["vimeo.com", "player.vimeo.com"]; + readonly isAspectRatioLocked: true; + readonly sizeToContentAspectRatio: true; + readonly title: 'Vimeo'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'vimeo'; + readonly width: 640; +}, { + readonly backgroundColor: '#fff'; + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 500; + readonly hostnames: readonly ["observablehq.com"]; + readonly isAspectRatioLocked: false; + readonly title: 'Observable'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'observable'; + readonly width: 720; +}, { + readonly doesResize: true; + readonly embedOnPaste: true; + readonly fromEmbedUrl: (url: string) => string | undefined; + readonly height: 450; + readonly hostnames: readonly ["desmos.com"]; + readonly title: 'Desmos'; + readonly toEmbedUrl: (url: string) => string | undefined; + readonly type: 'desmos'; + readonly width: 700; +}]; + +// @public +export const DEFAULT_MAX_ASSET_SIZE: number; + +// @public +export const DEFAULT_MAX_IMAGE_DIMENSION = 5000; + +// @public (undocumented) +export const DefaultA11yAnnouncer: MemoExoticComponent<() => "" | JSX.Element | undefined>; + +// @public (undocumented) +export const DefaultActionsMenu: MemoExoticComponent<({ children, }: TLUiActionsMenuProps) => JSX.Element | undefined>; + +// @public (undocumented) +export function DefaultActionsMenuContent(): JSX.Element; + +// @public (undocumented) +export function defaultAddFontsFromNode(node: Node_3, state: RichTextFontVisitorState, addFont: (font: TLFontFace_2) => void): RichTextFontVisitorState; + +// @public (undocumented) +export const defaultAssetUtils: readonly [typeof ImageAssetUtil, typeof VideoAssetUtil, typeof BookmarkAssetUtil]; + +// @public (undocumented) +export const defaultBindingUtils: readonly [typeof ArrowBindingUtil]; + +// @public (undocumented) +const DefaultContextMenu: MemoExoticComponent<({ children, disabled, }: TLUiContextMenuProps) => JSX.Element>; +export { DefaultContextMenu as ContextMenu } +export { DefaultContextMenu } + +// @public (undocumented) +export function DefaultContextMenuContent(): JSX.Element; + +// @public (undocumented) +export function DefaultDebugMenu({ children }: TLUiDebugMenuProps): JSX.Element; + +// @public (undocumented) +export function DefaultDebugMenuContent({ customDebugFlags, customFeatureFlags, }: CustomDebugFlags): JSX.Element; + +// @public (undocumented) +export const DefaultDialogs: MemoExoticComponent<() => JSX.Element[]>; + +// @public (undocumented) +export let defaultEditorAssetUrls: TLEditorAssetUrls; + +// @public +export interface DefaultEmbedConfig { + // (undocumented) + readonly google_maps?: GoogleMapsEmbedConfig; +} + +// @public (undocumented) +export type DefaultEmbedDefinitionType = (typeof DEFAULT_EMBED_DEFINITIONS)[number]['type']; + +// @public (undocumented) +export function DefaultFollowingIndicator(): JSX.Element | null; + +// @public (undocumented) +export const DefaultFontFaces: TLDefaultFonts; + +// @public +export const defaultGeoTypeDefinitions: { + readonly 'arrow-down': { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-arrow-down'; + readonly snapType: 'polygon'; + }; + readonly 'arrow-left': { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-arrow-left'; + readonly snapType: 'polygon'; + }; + readonly 'arrow-right': { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-arrow-right'; + readonly snapType: 'polygon'; + }; + readonly 'arrow-up': { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-arrow-up'; + readonly snapType: 'polygon'; + }; + readonly 'check-box': { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-check-box'; + readonly snapType: 'polygon'; + }; + readonly 'rhombus-2': { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-rhombus-2'; + readonly snapType: 'polygon'; + }; + readonly 'x-box': { + readonly getPath: (w: number, h: number, shape: TLGeoShape, strokeWidth: number) => PathBuilder; + readonly icon: 'geo-x-box'; + readonly snapType: 'polygon'; + }; + readonly cloud: { + readonly defaultSize: { + readonly h: 180; + readonly w: 300; + }; + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-cloud'; + readonly snapType: 'blobby'; + }; + readonly diamond: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-diamond'; + readonly snapType: 'polygon'; + }; + readonly ellipse: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-ellipse'; + readonly snapType: 'blobby'; + }; + readonly heart: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-heart'; + readonly snapType: 'blobby'; + }; + readonly hexagon: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-hexagon'; + readonly snapType: 'polygon'; + }; + readonly octagon: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-octagon'; + readonly snapType: 'polygon'; + }; + readonly oval: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-oval'; + readonly snapType: 'blobby'; + }; + readonly pentagon: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-pentagon'; + readonly snapType: 'polygon'; + }; + readonly rectangle: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-rectangle'; + readonly snapType: 'polygon'; + }; + readonly rhombus: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-rhombus'; + readonly snapType: 'polygon'; + }; + readonly star: { + readonly defaultSize: { + readonly h: 190; + readonly w: 200; + }; + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-star'; + readonly snapType: 'polygon'; + }; + readonly trapezoid: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-trapezoid'; + readonly snapType: 'polygon'; + }; + readonly triangle: { + readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder; + readonly icon: 'geo-triangle'; + readonly snapType: 'polygon'; + }; +}; + +// @public (undocumented) +export function defaultHandleExternalEmbedContent(editor: Editor, { point, url, embed }: { + embed: T; + point?: VecLike; + url: string; +}): void; + +// @public (undocumented) +export function defaultHandleExternalExcalidrawContent(editor: Editor, { point, content }: { + content: any; + point?: VecLike; +}): Promise; + +// @public (undocumented) +export function defaultHandleExternalFileAsset(editor: Editor, { file, assetId }: TLFileExternalAsset, options: TLDefaultExternalContentHandlerOpts): Promise>; + +// @public (undocumented) +export function defaultHandleExternalFileContent(editor: Editor, { point, files }: { + files: File[]; + point?: VecLike; +}, options: TLDefaultExternalContentHandlerOpts): Promise; + +// @public (undocumented) +export function defaultHandleExternalFileReplaceContent(editor: Editor, { file, shapeId }: TLFileReplaceExternalContent, options: TLDefaultExternalContentHandlerOpts): Promise; + +// @public (undocumented) +export function defaultHandleExternalSvgTextContent(editor: Editor, { point, text }: { + point?: VecLike; + text: string; +}): Promise; + +// @public (undocumented) +export function defaultHandleExternalTextContent(editor: Editor, { point, text, html }: { + html?: string; + point?: VecLike; + text: string; +}): Promise; + +// @public (undocumented) +export function defaultHandleExternalTldrawContent(editor: Editor, { point, content }: { + content: TLContent; + point?: VecLike; +}): Promise; + +// @public (undocumented) +export function defaultHandleExternalUrlAsset(editor: Editor, { url }: TLUrlExternalAsset, { toasts, msg }: TLDefaultExternalContentHandlerOpts): Promise; + +// @public (undocumented) +export function defaultHandleExternalUrlContent(editor: Editor, { point, url, shapeId }: { + point?: VecLike; + shapeId?: TLShapeId; + url: string; +}, { toasts, msg }: TLDefaultExternalContentHandlerOpts): Promise; + +// @public (undocumented) +export function DefaultHelperButtons({ children }: TLUiHelperButtonsProps): JSX.Element; + +// @public (undocumented) +export function DefaultHelperButtonsContent(): JSX.Element; + +// @public (undocumented) +export const DefaultHelpMenu: MemoExoticComponent<({ children }: TLUiHelpMenuProps) => JSX.Element | null>; + +// @public (undocumented) +export function DefaultHelpMenuContent(): JSX.Element; + +// @public (undocumented) +export function DefaultImageToolbar({ children }: TLUiImageToolbarProps): JSX.Element | null; + +// @public (undocumented) +export const DefaultImageToolbarContent: NamedExoticComponent; + +// @public (undocumented) +export interface DefaultImageToolbarContentProps { + // (undocumented) + imageShapeId: TLImageShape['id']; + // (undocumented) + isManipulating: boolean; + // (undocumented) + onEditAltTextStart(): void; + // (undocumented) + onManipulatingEnd(): void; + // (undocumented) + onManipulatingStart(): void; +} + +// @public (undocumented) +export const DefaultKeyboardShortcutsDialog: MemoExoticComponent<({ children, }: TLUiKeyboardShortcutsDialogProps) => JSX.Element>; + +// @public (undocumented) +export function DefaultKeyboardShortcutsDialogContent(): JSX.Element; + +// @public (undocumented) +export const DefaultMainMenu: MemoExoticComponent<({ children }: TLUiMainMenuProps) => JSX.Element>; + +// @public (undocumented) +export function DefaultMainMenuContent(): JSX.Element; + +// @public (undocumented) +export const DefaultMenuPanel: MemoExoticComponent<() => JSX.Element | null>; + +// @public (undocumented) +export function DefaultMinimap(): JSX.Element; + +// @public (undocumented) +export const DefaultNavigationPanel: MemoExoticComponent<() => JSX.Element | null>; + +// @public +export function DefaultNoteShapeAttribution({ name, firstName, color, scale, variant, }: TLNoteShapeAttributionProps): JSX.Element; + +// @public (undocumented) +export const defaultOverlayUtils: readonly [typeof ArrowBindingHintOverlayUtil, typeof ArrowHintOverlayUtil, typeof BrushOverlayUtil, typeof CollaboratorBrushOverlayUtil, typeof CollaboratorHintOverlayUtil, typeof CollaboratorScribbleOverlayUtil, typeof CollaboratorShapeIndicatorOverlayUtil, typeof ScribbleOverlayUtil, typeof SelectionForegroundOverlayUtil, typeof ShapeHandleOverlayUtil, typeof ShapeIndicatorOverlayUtil, typeof SnapIndicatorOverlayUtil, typeof ZoomBrushOverlayUtil]; + +// @public (undocumented) +export const DefaultPageMenu: MemoExoticComponent<() => JSX.Element>; + +// @public (undocumented) +function DefaultPeopleMenu({ children }: DefaultPeopleMenuProps): JSX.Element | null; +export { DefaultPeopleMenu } +export { DefaultPeopleMenu as PeopleMenu } + +// @public (undocumented) +export function DefaultPeopleMenuAvatar({ userId }: TLUiPeopleMenuAvatarProps): JSX.Element | null; + +// @public (undocumented) +export function DefaultPeopleMenuContent({ userIds }: DefaultPeopleMenuContentProps): JSX.Element; + +// @public (undocumented) +export interface DefaultPeopleMenuContentProps { + // (undocumented) + userIds: TLUserId[]; +} + +// @public (undocumented) +export function DefaultPeopleMenuFacePile({ userIds, userName, userColor, }: TLUiPeopleMenuFacePileProps): JSX.Element; + +// @public (undocumented) +export const DefaultPeopleMenuItem: NamedExoticComponent; + +// @public (undocumented) +interface DefaultPeopleMenuProps { + // (undocumented) + children?: ReactNode; +} +export { DefaultPeopleMenuProps } +export { DefaultPeopleMenuProps as PeopleMenuProps } + +// @public (undocumented) +export const DefaultQuickActions: MemoExoticComponent<({ children, }: TLUiQuickActionsProps) => JSX.Element>; + +// @public (undocumented) +export function DefaultQuickActionsContent(): JSX.Element | undefined; + +// @public +export const DefaultRichTextToolbar: React_3.NamedExoticComponent; + +// @public +export function DefaultRichTextToolbarContent({ textEditor, onEditLinkStart, }: DefaultRichTextToolbarContentProps): JSX.Element[]; + +// @public (undocumented) +export interface DefaultRichTextToolbarContentProps { + // (undocumented) + onEditLinkStart?(): void; + // (undocumented) + textEditor: TiptapEditor; +} + +// @public (undocumented) +export const defaultShapeTools: readonly [typeof TextShapeTool, typeof DrawShapeTool, typeof GeoShapeTool, typeof NoteShapeTool, typeof LineShapeTool, typeof FrameShapeTool, typeof ArrowShapeTool, typeof HighlightShapeTool]; + +// @public (undocumented) +export const defaultShapeUtils: readonly [typeof TextShapeUtil, typeof BookmarkShapeUtil, typeof DrawShapeUtil, typeof GeoShapeUtil, typeof NoteShapeUtil, typeof LineShapeUtil, typeof FrameShapeUtil, typeof ArrowShapeUtil, typeof HighlightShapeUtil, typeof EmbedShapeUtil, typeof ImageShapeUtil, typeof VideoShapeUtil]; + +// @public (undocumented) +export function DefaultSharePanel(): JSX.Element | null; + +// @public (undocumented) +export const DefaultStylePanel: MemoExoticComponent<({ isMobile, styles, children, }: TLUiStylePanelProps) => JSX.Element | null>; + +// @public (undocumented) +export function DefaultStylePanelContent(): JSX.Element; + +// @public (undocumented) +export const DefaultToasts: MemoExoticComponent<() => JSX.Element>; + +// @public +export const DefaultToolbar: MemoExoticComponent<({ children, orientation, minItems, minSizePx, maxItems, maxSizePx, }: DefaultToolbarProps) => JSX.Element>; + +// @public (undocumented) +export function DefaultToolbarContent(): JSX.Element; + +// @public (undocumented) +export interface DefaultToolbarProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + maxItems?: number; + // (undocumented) + maxSizePx?: number; + // (undocumented) + minItems?: number; + // (undocumented) + minSizePx?: number; + // (undocumented) + orientation?: 'horizontal' | 'vertical'; +} + +// @public (undocumented) +export const defaultTools: readonly [typeof EraserTool, typeof HandTool, typeof LaserTool, typeof ZoomTool, typeof SelectTool]; + +// @public (undocumented) +export function DefaultUserPresenceEditor(): JSX.Element; + +// @public (undocumented) +export const DefaultVideoToolbar: NamedExoticComponent; + +// @public (undocumented) +export const DefaultVideoToolbarContent: NamedExoticComponent; + +// @public (undocumented) +export interface DefaultVideoToolbarContentProps { + // (undocumented) + onEditAltTextStart(): void; + // (undocumented) + videoShapeId: TLVideoShape['id']; +} + +// @public +export const defaultWrappingPairs: Record; + +// @public (undocumented) +export const DefaultZoomMenu: MemoExoticComponent<({ children }: TLUiZoomMenuProps) => JSX.Element>; + +// @public (undocumented) +export function DefaultZoomMenuContent(): JSX.Element; + +// @public (undocumented) +export function DeleteMenuItem(): JSX.Element; + +// @public (undocumented) +export function DiamondToolbarItem(): JSX.Element; + +// @public (undocumented) +export function DistributeMenuItems(): JSX.Element; + +// @internal (undocumented) +export function downloadFile(file: File, doc?: Document): void; + +// @public +export function downsizeImage(blob: Blob, width: number, height: number, opts?: { + quality?: number; + type?: string; +}): Promise; + +// @public (undocumented) +export interface DrawPathBuilderDOpts { + // (undocumented) + offset?: number; + // (undocumented) + onlyFilled?: boolean; + // (undocumented) + passes?: number; + // (undocumented) + randomSeed: string; + // (undocumented) + roundness?: number; + // (undocumented) + strokeWidth: number; +} + +// @public (undocumented) +export interface DrawPathBuilderOpts extends BasePathBuilderOpts, DrawPathBuilderDOpts { + // (undocumented) + style: 'draw'; +} + +// @public (undocumented) +export interface DrawShapeOptions extends ShapeOptionsWithDisplayValues { + readonly maxPointsPerShape: number; +} + +// @public (undocumented) +export class DrawShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + onExit(): void; + // (undocumented) + shapeType: string; + // (undocumented) + static useCoalescedEvents: boolean; +} + +// @public (undocumented) +export class DrawShapeUtil extends ShapeUtil { + // (undocumented) + component(shape: TLDrawShape): JSX.Element; + // (undocumented) + expandSelectionOutlinePx(shape: TLDrawShape): number; + // (undocumented) + getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[]; + // (undocumented) + getDefaultProps(): TLDrawShape['props']; + // (undocumented) + getGeometry(shape: TLDrawShape): Circle2d | Polyline2d; + // (undocumented) + getIndicatorPath(shape: TLDrawShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLDrawShape, endShape: TLDrawShape, t: number): TLDrawShapeProps; + // (undocumented) + hideResizeHandles(shape: TLDrawShape): boolean; + // (undocumented) + hideRotateHandle(shape: TLDrawShape): boolean; + // (undocumented) + hideSelectionBoundsFg(shape: TLDrawShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onResize(shape: TLDrawShape, info: TLResizeInfo): { + props: { + scaleX: number; + scaleY: number; + }; + } | undefined; + // (undocumented) + options: DrawShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLDrawShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'draw'; +} + +// @public (undocumented) +export interface DrawShapeUtilDisplayValues { + // (undocumented) + fillColor: string; + // (undocumented) + patternFillFallbackColor: string; + // (undocumented) + strokeColor: string; + // (undocumented) + strokeWidth: number; +} + +// @public (undocumented) +export function DrawToolbarItem(): JSX.Element; + +// @public (undocumented) +export function DuplicateMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function EditLinkMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function EditMenuSubmenu(): JSX.Element | null; + +// @public (undocumented) +export function EditSubmenu(): JSX.Element; + +// @public (undocumented) +export interface ElbowArrowBox { + expanded: Box; + original: Box; +} + +// @public +export interface ElbowArrowBoxEdges { + // (undocumented) + bottom: ElbowArrowEdge | null; + // (undocumented) + left: ElbowArrowEdge | null; + // (undocumented) + right: ElbowArrowEdge | null; + // (undocumented) + top: ElbowArrowEdge | null; +} + +// @public (undocumented) +export interface ElbowArrowBoxes { + A: Box; + B: Box; + common: Box; +} + +// @public +export interface ElbowArrowEdge { + cross: ElbowArrowRange; + crossTarget: number; + expanded: null | number; + isPartial: boolean; + value: number; +} + +// @public (undocumented) +export interface ElbowArrowInfo extends ElbowArrowInfoWithoutRoute { + // (undocumented) + midXRange: { + hi: number; + lo: number; + } | null; + // (undocumented) + midYRange: { + hi: number; + lo: number; + } | null; + route: ElbowArrowRoute | null; +} + +// @public (undocumented) +export interface ElbowArrowInfoWithoutRoute { + A: ElbowArrowTargetBox; + B: ElbowArrowTargetBox; + common: ElbowArrowBox; + gapX: number; + gapY: number; + midX: null | number; + midY: null | number; + options: ElbowArrowOptions; + swapOrder: boolean; +} + +// @public +export interface ElbowArrowMidpointHandle { + // (undocumented) + axis: 'x' | 'y'; + point: VecLike; + segmentEnd: VecLike; + segmentStart: VecLike; +} + +// @public +export interface ElbowArrowOptions { + // (undocumented) + elbowMidpoint: number; + // (undocumented) + expandElbowLegLength: number; + // (undocumented) + minElbowLegLength: number; +} + +// @public (undocumented) +export interface ElbowArrowRange { + // (undocumented) + max: number; + // (undocumented) + min: number; +} + +// @public +export interface ElbowArrowRoute { + // @internal + aEdgePicking: ElbowArrowSideReason; + // @internal + bEdgePicking: ElbowArrowSideReason; + distance: number; + midpointHandle: ElbowArrowMidpointHandle | null; + // @internal + name: string; + points: Vec[]; + skipPointsWhenDrawing: Set; +} + +// @public +export type ElbowArrowSide = 'bottom' | 'left' | 'right' | 'top'; + +// @internal +export type ElbowArrowSideReason = 'auto' | 'fallback' | 'manual'; + +// @public (undocumented) +export interface ElbowArrowTargetBox extends ElbowArrowBox { + arrowheadOffset: number; + edges: ElbowArrowBoxEdges; + geometry: Geometry2d | null; + isExact: boolean; + isPoint: boolean; + minEndSegmentLength: number; + target: Vec; +} + +// @public (undocumented) +export function EllipseToolbarItem(): JSX.Element; + +// @public (undocumented) +export interface EmbedDefinition { + // (undocumented) + readonly backgroundColor?: string; + // (undocumented) + readonly canEditWhileLocked?: boolean; + // (undocumented) + readonly doesResize: boolean; + // (undocumented) + readonly embedOnPaste?: boolean; + // (undocumented) + readonly fromEmbedUrl: (url: string) => string | undefined; + // (undocumented) + readonly height: number; + // (undocumented) + readonly hostnames: readonly string[]; + // (undocumented) + readonly instructionLink?: string; + // (undocumented) + readonly isAspectRatioLocked?: boolean; + // (undocumented) + readonly minHeight?: number; + // (undocumented) + readonly minWidth?: number; + // (undocumented) + readonly overrideOutlineRadius?: number; + // (undocumented) + readonly overridePermissions?: TLEmbedShapePermissions; + readonly sizeToContentAspectRatio?: boolean; + // (undocumented) + readonly title: string; + // (undocumented) + readonly toEmbedUrl: (url: string, config?: Config) => string | undefined; + // (undocumented) + readonly type: string; + // (undocumented) + readonly width: number; +} + +// @public (undocumented) +export interface EmbedShapeOptions extends ShapeOptionsWithDisplayValues { + readonly embedConfig?: DefaultEmbedConfig & Record; + readonly embedDefinitions: readonly TLEmbedDefinition[]; +} + +// @public +export const embedShapePermissionDefaults: { + readonly 'allow-downloads-without-user-activation': false; + readonly 'allow-downloads': false; + readonly 'allow-forms': true; + readonly 'allow-modals': false; + readonly 'allow-orientation-lock': false; + readonly 'allow-pointer-lock': false; + readonly 'allow-popups-to-escape-sandbox': false; + readonly 'allow-popups': true; + readonly 'allow-presentation': false; + readonly 'allow-same-origin': true; + readonly 'allow-scripts': true; + readonly 'allow-storage-access-by-user-activation': false; + readonly 'allow-top-navigation-by-user-activation': false; + readonly 'allow-top-navigation': false; +}; + +// @public (undocumented) +export class EmbedShapeUtil extends BaseBoxShapeUtil { + // (undocumented) + canEdit(shape: TLEmbedShape): boolean; + // (undocumented) + canEditInReadonly(shape: TLEmbedShape): boolean; + // (undocumented) + canEditWhileLocked(shape: TLEmbedShape): boolean; + // (undocumented) + canResize(shape: TLEmbedShape): boolean; + // (undocumented) + component(shape: TLEmbedShape): JSX.Element | null; + // (undocumented) + getAriaDescriptor(shape: TLEmbedShape): string | undefined; + // (undocumented) + getDefaultProps(): TLEmbedShape['props']; + // (undocumented) + getEmbedDefinition(url: string): TLEmbedResult; + // (undocumented) + getEmbedDefinitions(): readonly TLEmbedDefinition[]; + // (undocumented) + getGeometry(shape: TLEmbedShape): Geometry2d; + // (undocumented) + getIndicatorPath(shape: TLEmbedShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLEmbedShape, endShape: TLEmbedShape, t: number): TLEmbedShapeProps; + // (undocumented) + getText(shape: TLEmbedShape): string; + // (undocumented) + hideSelectionBoundsFg(shape: TLEmbedShape): boolean; + // (undocumented) + isAspectRatioLocked(shape: TLEmbedShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onResize(shape: TLEmbedShape, info: TLResizeInfo): TLEmbedShape; + // (undocumented) + options: EmbedShapeOptions; + // (undocumented) + static props: RecordProps; + resolveAspectRatio(shape: TLEmbedShape): Promise; + // @deprecated (undocumented) + static setEmbedDefinitions(embedDefinitions: readonly TLEmbedDefinition[]): void; + // (undocumented) + static type: 'embed'; +} + +// @public (undocumented) +export interface EmbedShapeUtilDisplayValues { + // (undocumented) + showShadow: boolean; +} + +// @public (undocumented) +export class EraserTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + info: { + onInteractionEnd?: string; + }; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + maybeReturnToOriginatingTool(): void; + // (undocumented) + onEnter(info?: { + onInteractionEnd?: string; + }): void; + // (undocumented) + onExit(): void; +} + +// @public (undocumented) +export function EraserToolbarItem(): JSX.Element; + +// @public (undocumented) +export interface EventsProviderProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + onEvent?: TLUiEventHandler; +} + +// @public (undocumented) +export function ExampleDialog({ title, body, cancel, confirm, displayDontShowAgain, maxWidth, onCancel, onContinue, }: ExampleDialogProps): JSX.Element; + +// @public (undocumented) +export interface ExampleDialogProps { + // (undocumented) + body?: React_3.ReactNode; + // (undocumented) + cancel?: string; + // (undocumented) + confirm?: string; + // (undocumented) + displayDontShowAgain?: boolean; + // (undocumented) + maxWidth?: string; + // (undocumented) + onCancel(): void; + // (undocumented) + onContinue(): void; + // (undocumented) + title?: string; +} + +// @public +export function exportAs(editor: Editor, ids: TLShapeId[], opts: ExportAsOptions): Promise; + +// @public (undocumented) +export function ExportAsMenuGroup(): JSX.Element | null; + +// @public (undocumented) +export interface ExportAsOptions extends TLImageExportOptions { + format: TLExportType; + name?: string; +} + +// @public (undocumented) +export function ExportFileContentSubMenu(): JSX.Element | null; + +export { Extension } + +export { Extensions } + +// @public (undocumented) +export function ExtrasGroup(): JSX.Element; + +// @public (undocumented) +export function FeatureFlags(props: FeatureFlagsProps): JSX.Element | null; + +// @public (undocumented) +export interface FeatureFlagsProps { + // (undocumented) + customFeatureFlags?: Record> | undefined; +} + +// @public +export function fitFrameToContent(editor: Editor, id: TLShapeId, opts?: { + padding: number; +}): void; + +// @public (undocumented) +export function FitFrameToContentMenuItem(): JSX.Element | null; + +// @public (undocumented) +export interface FrameShapeOptions extends ShapeOptionsWithDisplayValues { + resizeChildren: boolean; + showColors: boolean; +} + +// @public (undocumented) +export class FrameShapeTool extends BaseBoxShapeTool { + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + onCreate(shape: null | TLShape): void; + // (undocumented) + shapeType: 'frame'; +} + +// @public (undocumented) +export class FrameShapeUtil extends BaseFrameLikeShapeUtil { + // (undocumented) + canEdit(shape: TLFrameShape, info: TLEditStartInfo): boolean; + // (undocumented) + canResize(shape: TLFrameShape): boolean; + // (undocumented) + canResizeChildren(shape: TLFrameShape): boolean; + // (undocumented) + component(shape: TLFrameShape): JSX.Element; + // (undocumented) + static configure>(this: T, options: T extends new (...args: any[]) => { + options: infer Options; + } ? Partial : never): T; + // (undocumented) + getAriaDescriptor(shape: TLFrameShape): string; + // (undocumented) + getDefaultProps(): TLFrameShape['props']; + // (undocumented) + getGeometry(shape: TLFrameShape): Geometry2d; + // (undocumented) + getIndicatorPath(shape: TLFrameShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLFrameShape, endShape: TLFrameShape, t: number): TLFrameShapeProps; + // (undocumented) + getText(shape: TLFrameShape): string | undefined; + // (undocumented) + isExportBoundsContainer(): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onDoubleClickCorner(shape: TLFrameShape): { + id: TLShapeId_2; + type: "frame"; + }; + // (undocumented) + onDoubleClickEdge(shape: TLFrameShape, info: TLClickEventInfo): { + id: TLShapeId_2; + props: { + h: number; + w: number; + }; + type: "frame"; + x: number; + y: number; + } | undefined; + // (undocumented) + options: FrameShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLFrameShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'frame'; +} + +// @public (undocumented) +export interface FrameShapeUtilDisplayValues { + // (undocumented) + fillColor: string; + // (undocumented) + headingFillColor: string; + // (undocumented) + headingStrokeColor: string; + // (undocumented) + headingTextColor: string; + // (undocumented) + showColorsFillColor: string; + // (undocumented) + showColorsHeadingFillColor: string; + // (undocumented) + showColorsHeadingStrokeColor: string; + // (undocumented) + showColorsHeadingTextColor: string; + // (undocumented) + showColorsStrokeColor: string; + // (undocumented) + strokeColor: string; +} + +// @public (undocumented) +export function FrameToolbarItem(): JSX.Element; + +// @public (undocumented) +export interface GeoShapeOptions extends ShapeOptionsWithDisplayValues { + customGeoTypes?: Record; + // (undocumented) + showTextOutline: boolean; +} + +// @public (undocumented) +export class GeoShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + shapeType: string; +} + +// @public (undocumented) +export class GeoShapeUtil extends BaseBoxShapeUtil { + // (undocumented) + canEdit(shape: TLGeoShape): boolean; + // (undocumented) + component(shape: TLGeoShape): JSX.Element; + // (undocumented) + static configure>(this: T, options: T extends new (...args: any[]) => { + options: infer Options; + } ? Partial : never): T; + // (undocumented) + getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[]; + // (undocumented) + getDefaultProps(): TLGeoShape['props']; + // (undocumented) + getFontFaces(shape: TLGeoShape): TLFontFace[]; + // (undocumented) + getGeometry(shape: TLGeoShape): Group2d; + // (undocumented) + getHandleSnapGeometry(shape: TLGeoShape): HandleSnapGeometry; + // (undocumented) + getIndicatorPath(shape: TLGeoShape): Path2D | undefined; + // (undocumented) + getInterpolatedProps(startShape: TLGeoShape, endShape: TLGeoShape, t: number): TLGeoShapeProps; + // (undocumented) + getText(shape: TLGeoShape): string; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onBeforeCreate(shape: TLGeoShape): { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + align: TLDefaultHorizontalAlignStyle; + color: TLDefaultColorStyle; + dash: TLDefaultDashStyle; + fill: TLDefaultFillStyle; + flipX: boolean; + flipY: boolean; + font: TLDefaultFontStyle; + geo: TLGeoShapeGeoStyle; + growY: number; + h: number; + labelColor: TLDefaultColorStyle; + richText: TLRichText; + scale: number; + size: TLDefaultSizeStyle; + url: string; + verticalAlign: TLDefaultVerticalAlignStyle; + w: number; + }; + rotation: number; + type: "geo"; + typeName: 'shape'; + x: number; + y: number; + } | undefined; + // (undocumented) + onBeforeUpdate(prev: TLGeoShape, next: TLGeoShape): { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + align: TLDefaultHorizontalAlignStyle; + color: TLDefaultColorStyle; + dash: TLDefaultDashStyle; + fill: TLDefaultFillStyle; + flipX: boolean; + flipY: boolean; + font: TLDefaultFontStyle; + geo: TLGeoShapeGeoStyle; + growY: number; + h: number; + labelColor: TLDefaultColorStyle; + richText: TLRichText; + scale: number; + size: TLDefaultSizeStyle; + url: string; + verticalAlign: TLDefaultVerticalAlignStyle; + w: number; + }; + rotation: number; + type: "geo"; + typeName: 'shape'; + x: number; + y: number; + } | undefined; + // (undocumented) + onDoubleClick(shape: TLGeoShape): { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + align: TLDefaultHorizontalAlignStyle; + color: TLDefaultColorStyle; + dash: TLDefaultDashStyle; + fill: TLDefaultFillStyle; + flipX: boolean; + flipY: boolean; + font: TLDefaultFontStyle; + geo: TLGeoShapeGeoStyle; + growY: number; + h: number; + labelColor: TLDefaultColorStyle; + richText: TLRichText; + scale: number; + size: TLDefaultSizeStyle; + url: string; + verticalAlign: TLDefaultVerticalAlignStyle; + w: number; + }; + rotation: number; + type: "geo"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + geo: 'check-box'; + }; + rotation: number; + type: "geo"; + typeName: 'shape'; + x: number; + y: number; + } | { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + geo: 'rectangle'; + }; + rotation: number; + type: "geo"; + typeName: 'shape'; + x: number; + y: number; + } | undefined; + // (undocumented) + onResize(shape: TLGeoShape, { handle, newPoint, scaleX, scaleY, initialShape }: TLResizeInfo): { + props: { + flipX: boolean; + flipY: boolean; + growY: number; + h: number; + w: number; + }; + x: number; + y: number; + }; + // (undocumented) + options: GeoShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLGeoShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'geo'; +} + +// @public (undocumented) +export interface GeoShapeUtilDisplayValues { + // (undocumented) + fillColor: string; + // (undocumented) + labelColor: string; + // (undocumented) + labelEdgeMargin: number; + // (undocumented) + labelExtraPadding: number; + // (undocumented) + labelFontFamily: string; + // (undocumented) + labelFontSize: number; + // (undocumented) + labelFontStyle: string; + // (undocumented) + labelFontVariant: string; + // (undocumented) + labelFontWeight: string; + // (undocumented) + labelHorizontalAlign: 'center' | 'end' | 'start'; + // (undocumented) + labelLineHeight: number; + // (undocumented) + labelMinWidth: number; + // (undocumented) + labelPadding: number; + // (undocumented) + labelVerticalAlign: 'end' | 'middle' | 'start'; + // (undocumented) + minSizeWithLabel: number; + // (undocumented) + patternFillFallbackColor: string; + // (undocumented) + strokeColor: string; + // (undocumented) + strokeRoundness: number; + // (undocumented) + strokeWidth: number; +} + +// @public +export interface GeoTypeDefinition { + defaultSize?: { + h: number; + w: number; + }; + getPath(w: number, h: number, shape: TLGeoShape, strokeWidth: number): PathBuilder; + icon: string; + onDoubleClick?(shape: TLGeoShape): { + props: Partial; + } | void; + snapType: 'blobby' | 'polygon'; +} + +// @public (undocumented) +export function getArrowBindings(editor: Editor, shape: TLArrowShape): TLArrowBindings; + +// @public (undocumented) +export function getArrowInfo(editor: Editor, shape: TLArrowShape | TLShapeId): TLArrowInfo | undefined; + +// @public +export function getArrowTargetState(editor: Editor): ArrowTargetState | null; + +// @public (undocumented) +export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShape, bindings: TLArrowBindings): { + end: Vec; + start: Vec; +}; + +// @public (undocumented) +export function getAssetInfo(editor: Editor, file: File, assetId?: TLAssetId): Promise; + +// @public +export function getColorStyleItems(colors: TLThemeColors): StyleValuesForUi; + +// @public (undocumented) +export function getCropBox(shape: T, info: TLCropInfo, opts?: CropBoxOptions): { + id: TLShapeId; + props: ShapeWithCrop['props']; + type: T['type']; + x: number; + y: number; +} | undefined; + +// @public (undocumented) +export function getDefaultCrop(): TLShapeCrop; + +// @public +export function getDisplayValues(util: { + editor: Editor; + options: ShapeOptionsWithDisplayValues; +}, shape: Shape, colorMode?: 'dark' | 'light'): DisplayValues; + +// @public +export function getEmbedInfo(definitions: readonly TLEmbedDefinition[], inputUrl: string, embedConfig?: Record): TLEmbedResult; + +// @public (undocumented) +export function getFontFamily(theme: TLTheme, font: string): string; + +// @public +export function getFontStyleItems(theme: TLTheme): StyleValuesForUi; + +// @public +export function getGeoTypeDefinition(name: string, customGeoTypes?: Record): GeoTypeDefinition | undefined; + +// @public (undocumented) +export function getHitShapeOnCanvasPointerDown(editor: Editor, hitLabels?: boolean): TLShape | undefined; + +// @public (undocumented) +export function getPointsFromDrawSegment(segment: TLDrawShapeSegment, scaleX: number, scaleY: number, points?: Vec[]): Vec[]; + +// @public (undocumented) +export function getPointsFromDrawSegments(segments: TLDrawShapeSegment[], scaleX?: number, scaleY?: number): Vec[]; + +export { getSchema } + +// @public +export function getStroke(points: VecLike[], options?: StrokeOptions): Vec[]; + +// @public +export function getStrokeOutlinePoints(strokePoints: StrokePoint[], options?: StrokeOptions): Vec[]; + +// @public +export function getStrokePoints(rawInputPoints: VecLike[], options?: StrokeOptions): StrokePoint[]; + +// @public +export function getSvgPathFromStrokePoints(points: StrokePoint[], closed?: boolean): string; + +// @public +export function getTipTapDefaultExtensions(starterKitOptions?: Partial): Extensions; + +// @public +export function getUncroppedSize(shapeSize: { + h: number; + w: number; +}, crop: null | TLShapeCrop): { + h: number; + w: number; +}; + +// @public +export interface GoogleMapsEmbedConfig { + // (undocumented) + readonly apiKey?: string; +} + +// @public (undocumented) +export function GroupMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function GroupOrUngroupMenuItem(): JSX.Element; + +// @public +export function handleNativeOrMenuCopy(editor: Editor, context?: TLClipboardWriteInfo): Promise; + +// @public (undocumented) +export class HandTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + onDoubleClick(info: TLClickEventInfo): void; +} + +// @public (undocumented) +export function HandToolbarItem(): JSX.Element; + +// @public (undocumented) +export function HeartToolbarItem(): JSX.Element; + +// @public (undocumented) +export function HexagonToolbarItem(): JSX.Element; + +// @public (undocumented) +export function hideAllTooltips(): void; + +// @public (undocumented) +export interface HighlightShapeOptions extends ShapeOptionsWithDisplayValues { + readonly maxPointsPerShape: number; +} + +// @public (undocumented) +export class HighlightShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + onExit(): void; + // (undocumented) + shapeType: string; + // (undocumented) + static useCoalescedEvents: boolean; +} + +// @public (undocumented) +export class HighlightShapeUtil extends ShapeUtil { + // (undocumented) + backgroundComponent(shape: TLHighlightShape): JSX.Element; + // (undocumented) + component(shape: TLHighlightShape): JSX.Element; + // (undocumented) + getDefaultProps(): TLHighlightShape['props']; + // (undocumented) + getGeometry(shape: TLHighlightShape): Circle2d | Polygon2d; + // (undocumented) + getIndicatorPath(shape: TLHighlightShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLHighlightShape, endShape: TLHighlightShape, t: number): TLHighlightShapeProps; + // (undocumented) + hideResizeHandles(shape: TLHighlightShape): boolean; + // (undocumented) + hideRotateHandle(shape: TLHighlightShape): boolean; + // (undocumented) + hideSelectionBoundsFg(shape: TLHighlightShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onResize(shape: TLHighlightShape, info: TLResizeInfo): { + props: { + scaleX: number; + scaleY: number; + }; + } | undefined; + // (undocumented) + options: HighlightShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toBackgroundSvg(shape: TLHighlightShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + toSvg(shape: TLHighlightShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'highlight'; +} + +// @public (undocumented) +export interface HighlightShapeUtilDisplayValues { + // (undocumented) + overlayOpacity: number; + // (undocumented) + strokeColor: string; + // (undocumented) + strokeWidth: number; + // (undocumented) + underlayOpacity: number; +} + +// @public (undocumented) +export function HighlightToolbarItem(): JSX.Element; + +// @public (undocumented) +export const iconTypes: readonly ['align-bottom', 'align-center-horizontal', 'align-center-vertical', 'align-left', 'align-right', 'align-top', 'alt', 'arrow-arc', 'arrow-cycle', 'arrow-elbow', 'arrow-left', 'arrowhead-arrow', 'arrowhead-bar', 'arrowhead-diamond', 'arrowhead-dot', 'arrowhead-none', 'arrowhead-square', 'arrowhead-triangle-inverted', 'arrowhead-triangle', 'blob', 'bold', 'bookmark', 'bring-forward', 'bring-to-front', 'broken', 'bulletList', 'check-circle', 'check', 'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'chevrons-ne', 'chevrons-sw', 'clipboard-copied', 'clipboard-copy', 'closed', 'code', 'color', 'comment', 'corners', 'crop', 'cross-2', 'cross-circle', 'dash-dashed', 'dash-dotted', 'dash-draw', 'dash-solid', 'disconnected', 'discord', 'distribute-horizontal', 'distribute-vertical', 'dot', 'dots-horizontal', 'dots-vertical', 'download', 'drag-handle-dots', 'duplicate', 'edit', 'external-link', 'fill-fill', 'fill-lined-fill', 'fill-none', 'fill-pattern', 'fill-semi', 'fill-solid', 'follow', 'following', 'font-draw', 'font-mono', 'font-sans', 'font-serif', 'geo-arrow-down', 'geo-arrow-left', 'geo-arrow-right', 'geo-arrow-up', 'geo-check-box', 'geo-cloud', 'geo-diamond', 'geo-ellipse', 'geo-heart', 'geo-hexagon', 'geo-octagon', 'geo-oval', 'geo-pentagon', 'geo-rectangle', 'geo-rhombus-2', 'geo-rhombus', 'geo-star', 'geo-trapezoid', 'geo-triangle', 'geo-x-box', 'github', 'group', 'heading', 'help-circle', 'highlight', 'horizontal-align-end', 'horizontal-align-middle', 'horizontal-align-start', 'info-circle', 'italic', 'leading', 'link', 'list', 'lock', 'manual', 'menu', 'minus', 'mixed', 'pack', 'plus', 'question-mark-circle', 'question-mark', 'redo', 'reset-zoom', 'rotate-ccw', 'rotate-cw', 'send-backward', 'send-to-back', 'share-1', 'size-extra-large', 'size-large', 'size-medium', 'size-small', 'spline-cubic', 'spline-line', 'stack-horizontal', 'stack-vertical', 'status-offline', 'stretch-horizontal', 'stretch-vertical', 'strike', 'text-align-center', 'text-align-left', 'text-align-right', 'toggle-off', 'toggle-on', 'tool-arrow', 'tool-eraser', 'tool-frame', 'tool-hand', 'tool-highlight', 'tool-laser', 'tool-line', 'tool-media', 'tool-note', 'tool-pencil', 'tool-pointer', 'tool-screenshot', 'tool-text', 'trash', 'twitter', 'underline', 'undo', 'ungroup', 'unlock', 'vertical-align-end', 'vertical-align-middle', 'vertical-align-start', 'warning-triangle', 'zoom-in', 'zoom-out']; + +// @public (undocumented) +export class ImageAssetUtil extends AssetUtil { + // (undocumented) + getAssetFromFile(file: File, assetId: TLAssetId): Promise; + // (undocumented) + getDefaultProps(): TLImageAsset['props']; + // (undocumented) + getSupportedMimeTypes(): readonly string[]; + // (undocumented) + static migrations: MigrationSequence; + // (undocumented) + options: { + maxDimension: number; + supportedMimeTypes: null | readonly string[]; + }; + // (undocumented) + static props: { + fileSize: Validator; + h: Validator; + isAnimated: Validator; + mimeType: Validator; + name: Validator; + pixelRatio: Validator; + src: Validator; + w: Validator; + }; + // (undocumented) + static type: 'image'; +} + +// @public (undocumented) +export interface ImageShapeOptions extends ShapeOptionsWithDisplayValues { +} + +// @public (undocumented) +export class ImageShapeUtil extends BaseBoxShapeUtil { + // (undocumented) + canCrop(shape: TLImageShape): boolean; + // (undocumented) + component(shape: TLImageShape): JSX.Element; + // (undocumented) + createShapeForAsset(asset: TLAsset, position: VecModel): null | TLShapePartial; + // (undocumented) + getAriaDescriptor(shape: TLImageShape): string; + // (undocumented) + getDefaultProps(): TLImageShape['props']; + // (undocumented) + getGeometry(shape: TLImageShape): Geometry2d; + // (undocumented) + getIndicatorPath(shape: TLImageShape): Path2D | undefined; + // (undocumented) + getInterpolatedProps(startShape: TLImageShape, endShape: TLImageShape, t: number): TLImageShapeProps; + // (undocumented) + static handledAssetTypes: readonly ['image']; + // (undocumented) + isAspectRatioLocked(shape: TLImageShape): boolean; + // (undocumented) + isExportBoundsContainer(): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onDoubleClickEdge(shape: TLImageShape): void; + // (undocumented) + onResize(shape: TLImageShape, info: TLResizeInfo): TLImageShape; + // (undocumented) + options: ImageShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLImageShape, ctx: SvgExportContext): Promise; + // (undocumented) + static type: 'image'; +} + +// @public (undocumented) +export interface ImageShapeUtilDisplayValues { +} + +// @public (undocumented) +export function InputModeMenu(): JSX.Element; + +export { JSONContent } + +// @public (undocumented) +export const KeyboardShiftEnterTweakExtension: Extension; + +// @public (undocumented) +export function KeyboardShortcutsMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function LanguageMenu(): JSX.Element | null; + +// @public (undocumented) +export class LaserTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + getSessionId(): string; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + onCancel(): void; + // (undocumented) + onEnter(): void; + // (undocumented) + onExit(): void; +} + +// @public (undocumented) +export function LaserToolbarItem(): JSX.Element; + +// @public (undocumented) +export interface LineShapeOptions extends ShapeOptionsWithDisplayValues { +} + +// @public (undocumented) +export class LineShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + shapeType: string; +} + +// @public (undocumented) +export class LineShapeUtil extends ShapeUtil { + // (undocumented) + component(shape: TLLineShape): JSX.Element; + // (undocumented) + getDefaultProps(): TLLineShape['props']; + // (undocumented) + getGeometry(shape: TLLineShape): PathBuilderGeometry2d; + // (undocumented) + getHandles(shape: TLLineShape): TLHandle[]; + // (undocumented) + getHandleSnapGeometry(shape: TLLineShape): HandleSnapGeometry; + // (undocumented) + getIndicatorPath(shape: TLLineShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLLineShape, endShape: TLLineShape, t: number): TLLineShape['props']; + // (undocumented) + hideInMinimap(): boolean; + // (undocumented) + hideResizeHandles(shape: TLLineShape): boolean; + // (undocumented) + hideRotateHandle(shape: TLLineShape): boolean; + // (undocumented) + hideSelectionBoundsBg(shape: TLLineShape): boolean; + // (undocumented) + hideSelectionBoundsFg(shape: TLLineShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onBeforeCreate(next: TLLineShape): TLLineShape | void; + // (undocumented) + onHandleDrag(shape: TLLineShape, { handle }: TLHandleDragInfo): { + id: TLShapeId_2; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + color: TLDefaultColorStyle; + dash: TLDefaultDashStyle; + points: { + [x: string]: { + id: string; + index: IndexKey; + x: number; + y: number; + } | TLLineShapePoint; + }; + scale: number; + size: TLDefaultSizeStyle; + spline: TLLineShapeSplineStyle; + }; + rotation: number; + type: "line"; + typeName: 'shape'; + x: number; + y: number; + }; + // (undocumented) + onHandleDragStart(shape: TLLineShape, { handle }: TLHandleDragInfo): { + id: TLShapeId_2; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + color: TLDefaultColorStyle; + dash: TLDefaultDashStyle; + points: { + [x: string]: { + id: IndexKey; + index: IndexKey; + x: number; + y: number; + } | TLLineShapePoint; + }; + scale: number; + size: TLDefaultSizeStyle; + spline: TLLineShapeSplineStyle; + }; + rotation: number; + type: "line"; + typeName: 'shape'; + x: number; + y: number; + } | undefined; + // (undocumented) + onResize(shape: TLLineShape, info: TLResizeInfo): { + props: { + points: { + [x: string]: { + id: string; + index: IndexKey; + x: number; + y: number; + }; + }; + }; + }; + // (undocumented) + options: LineShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLLineShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'line'; +} + +// @public (undocumented) +export interface LineShapeUtilDisplayValues { + // (undocumented) + strokeColor: string; + // (undocumented) + strokeWidth: number; +} + +// @public (undocumented) +export function LineToolbarItem(): JSX.Element; + +// @internal (undocumented) +export interface LineToPathBuilderCommand extends PathBuilderCommandBase { + // (undocumented) + type: 'line'; +} + +// @public (undocumented) +export function LockGroup(): JSX.Element; + +export { Mark } + +export { mergeAttributes } + +// @public (undocumented) +export function MiscMenuGroup(): JSX.Element; + +// @public (undocumented) +export function MobileStylePanel(): JSX.Element | null; + +// @public (undocumented) +export function MoveToPageMenu(): JSX.Element | null; + +// @internal (undocumented) +export interface MoveToPathBuilderCommand extends PathBuilderCommandBase { + // (undocumented) + closeIdx: null | number; + // (undocumented) + opts?: PathBuilderLineOpts; + // (undocumented) + type: 'move'; +} + +export { Node_2 as Node } + +// @public (undocumented) +export interface NonePathBuilderOpts extends BasePathBuilderOpts { + // (undocumented) + style: 'none'; +} + +// @public (undocumented) +export interface NoteShapeOptions extends ShapeOptionsWithDisplayValues { + AttributionComponent: TLNoteShapeAttributionComponent; + resizeMode: 'none' | 'scale'; +} + +// @public (undocumented) +export class NoteShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + shapeType: string; +} + +// @public (undocumented) +export class NoteShapeUtil extends ShapeUtil { + // (undocumented) + canEdit(shape: TLNoteShape): boolean; + // (undocumented) + component(shape: TLNoteShape): JSX.Element; + // (undocumented) + getDefaultProps(): TLNoteShape['props']; + // (undocumented) + getFontFaces(shape: TLNoteShape): TLFontFace[]; + // (undocumented) + getGeometry(shape: TLNoteShape): Group2d; + // (undocumented) + getHandles(shape: TLNoteShape): TLHandle[]; + // (undocumented) + getIndicatorPath(shape: TLNoteShape): Path2D; + // (undocumented) + getInterpolatedProps(startShape: TLNoteShape, endShape: TLNoteShape, t: number): TLNoteShapeProps; + // (undocumented) + getReferencedUserIds(shape: TLNoteShape): string[]; + // (undocumented) + getText(shape: TLNoteShape): string; + // (undocumented) + hideResizeHandles(shape: TLNoteShape): boolean; + // (undocumented) + hideSelectionBoundsFg(shape: TLNoteShape): boolean; + // (undocumented) + isAspectRatioLocked(shape: TLNoteShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onBeforeCreate(next: TLNoteShape): { + id: TLShapeId; + index: IndexKey_2; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + align: TLDefaultHorizontalAlignStyle; + color: TLDefaultColorStyle; + font: TLDefaultFontStyle; + fontSizeAdjustment: number; + growY: number; + labelColor: TLDefaultColorStyle; + richText: TLRichText; + scale: number; + size: TLDefaultSizeStyle; + textLastEditedBy: null | string; + url: string; + verticalAlign: TLDefaultVerticalAlignStyle; + }; + rotation: number; + type: "note"; + typeName: 'shape'; + x: number; + y: number; + } | undefined; + // (undocumented) + onBeforeDuplicate(_source: TLNoteShape, duplicate: TLNoteShape): TLNoteShape | undefined; + // (undocumented) + onBeforeUpdate(prev: TLNoteShape, next: TLNoteShape): TLNoteShape | undefined; + // (undocumented) + onResize(shape: any, info: TLResizeInfo): { + props: { + scale: number; + }; + x: number; + y: number; + } | undefined; + // (undocumented) + options: NoteShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLNoteShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'note'; +} + +// @public (undocumented) +export interface NoteShapeUtilDisplayValues { + // (undocumented) + borderColor: string; + // (undocumented) + borderWidth: number; + // (undocumented) + labelColor: string; + // (undocumented) + labelFontFamily: string; + // (undocumented) + labelFontSize: number; + // (undocumented) + labelFontStyle: string; + // (undocumented) + labelFontVariant: string; + // (undocumented) + labelFontWeight: string; + // (undocumented) + labelHorizontalAlign: 'center' | 'end' | 'start'; + // (undocumented) + labelLineHeight: number; + // (undocumented) + labelPadding: number; + // (undocumented) + labelVerticalAlign: 'end' | 'middle' | 'start'; + // (undocumented) + noteBackgroundColor: string; + // (undocumented) + noteHeight: number; + // (undocumented) + noteWidth: number; +} + +// @public (undocumented) +export function NoteToolbarItem(): JSX.Element; + +// @public +export function notifyIfFileNotAllowed(editor: Editor, file: File, options: TLDefaultExternalContentHandlerOpts): boolean; + +// @public (undocumented) +export function OfflineIndicator(): JSX.Element; + +// @public +export function onDragFromToolbarToCreateShape(editor: Editor, info: TLPointerEventInfo, opts: OnDragFromToolbarToCreateShapesOpts): void; + +// @public +export interface OnDragFromToolbarToCreateShapesOpts { + createShape(id: TLShapeId): void; + maskedToolId?: string; + onDragEnd?(id: TLShapeId): void; +} + +// @public (undocumented) +export function OvalToolbarItem(): JSX.Element; + +// @public (undocumented) +export function OverflowingToolbar({ children, orientation, sizingParentClassName, minItems, minSizePx, maxItems, maxSizePx, }: OverflowingToolbarProps): JSX.Element; + +// @public (undocumented) +export interface OverflowingToolbarProps { + // (undocumented) + children: React.ReactNode; + // (undocumented) + maxItems: number; + // (undocumented) + maxSizePx: number; + // (undocumented) + minItems: number; + // (undocumented) + minSizePx: number; + // (undocumented) + orientation: 'horizontal' | 'vertical'; + // (undocumented) + sizingParentClassName: string; +} + +// @public (undocumented) +export const PageItemInput: ({ name, id, isCurrentPage, onCancel, onComplete, }: PageItemInputProps) => JSX.Element; + +// @public (undocumented) +export interface PageItemInputProps { + // (undocumented) + id: TLPageId; + // (undocumented) + isCurrentPage: boolean; + // (undocumented) + name: string; + // (undocumented) + onCancel(): void; + // (undocumented) + onComplete?(): void; +} + +// @public (undocumented) +export const PageItemSubmenu: NamedExoticComponent; + +// @public (undocumented) +export interface PageItemSubmenuProps { + // (undocumented) + index: number; + // (undocumented) + item: { + id: string; + name: string; + }; + // (undocumented) + listSize: number; + // (undocumented) + onRename?(): void; +} + +// @internal (undocumented) +export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: Exclude | TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise; + +// @public (undocumented) +export function parseTldrawJsonFile({ json, schema, }: { + json: string; + schema: TLSchema; +}): Result; + +// @public (undocumented) +export function PasteMenuItem(): JSX.Element; + +// @public (undocumented) +export class PathBuilder { + constructor(); + // (undocumented) + arcTo(rx: number, ry: number, largeArcFlag: boolean, sweepFlag: boolean, xAxisRotationRadians: number, x2: number, y2: number, opts?: PathBuilderCommandOpts): this; + // (undocumented) + circularArcTo(radius: number, largeArcFlag: boolean, sweepFlag: boolean, x2: number, y2: number, opts?: PathBuilderCommandOpts): this; + // (undocumented) + close(): this; + // @internal (undocumented) + commands: PathBuilderCommand[]; + // (undocumented) + cubicBezierTo(x: number, y: number, cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, opts?: PathBuilderCommandOpts): this; + // (undocumented) + static cubicSplineThroughPoints(points: VecLike[], opts?: PathBuilderLineOpts & { + endOffsets?: number; + }): PathBuilder; + // @internal (undocumented) + getCommandInfo(): (PathBuilderCommandInfo | undefined)[]; + // @internal (undocumented) + getCommands(): readonly PathBuilderCommand[]; + // (undocumented) + static lineThroughPoints(points: VecLike[], opts?: PathBuilderLineOpts & { + endOffsets?: number; + }): PathBuilder; + // (undocumented) + lineTo(x: number, y: number, opts?: PathBuilderCommandOpts): this; + // (undocumented) + moveTo(x: number, y: number, opts?: PathBuilderLineOpts): this; + // (undocumented) + toD(opts?: PathBuilderToDOpts): string; + // (undocumented) + toDrawD(opts: DrawPathBuilderDOpts): string; + // (undocumented) + toGeometry(): Group2d | PathBuilderGeometry2d; + // (undocumented) + toPath2D(opts: PathBuilderOpts): Path2D; + // (undocumented) + toSvg(opts: PathBuilderOpts): JSX.Element | null; + transform(mat: MatLike): PathBuilder; +} + +// @internal (undocumented) +export type PathBuilderCommand = CubicBezierToPathBuilderCommand | LineToPathBuilderCommand | MoveToPathBuilderCommand; + +// @internal (undocumented) +export interface PathBuilderCommandBase { + // (undocumented) + _info?: PathBuilderCommandInfo; + // (undocumented) + isClose: boolean; + // (undocumented) + opts?: PathBuilderCommandOpts; + // (undocumented) + x: number; + // (undocumented) + y: number; +} + +// @internal (undocumented) +export interface PathBuilderCommandInfo { + // (undocumented) + length: number; + // (undocumented) + tangentEnd: VecModel; + // (undocumented) + tangentStart: VecModel; +} + +// @public (undocumented) +export interface PathBuilderCommandOpts { + mergeWithPrevious?: boolean; + offset?: number; + roundness?: number; +} + +// @public (undocumented) +export class PathBuilderGeometry2d extends Geometry2d { + constructor(path: PathBuilder, startIdx: number, endIdx: number, options: Geometry2dOptions); + // (undocumented) + getSegments(): Geometry2d[]; + // (undocumented) + getSvgPathData(): string; + // (undocumented) + getVertices(filters: Geometry2dFilters): Vec[]; + // (undocumented) + hitTestLineSegment(A: VecLike, B: VecLike, distance?: number, filters?: Geometry2dFilters): boolean; + // (undocumented) + nearestPoint(point: VecLike, _filters?: Geometry2dFilters): Vec; +} + +// @public (undocumented) +export interface PathBuilderLineOpts extends PathBuilderCommandOpts { + // (undocumented) + dashEnd?: PerfectDashTerminal; + // (undocumented) + dashStart?: PerfectDashTerminal; + // (undocumented) + geometry?: false | Omit; +} + +// @public (undocumented) +export type PathBuilderOpts = DashedPathBuilderOpts | DrawPathBuilderOpts | NonePathBuilderOpts | SolidPathBuilderOpts; + +// @public (undocumented) +export interface PathBuilderToDOpts { + // (undocumented) + endIdx?: number; + // (undocumented) + onlyFilled?: boolean; + // (undocumented) + startIdx?: number; +} + +// @public +export const PlainTextArea: React_3.ForwardRefExoticComponent>; + +// @public +export const PlainTextLabel: React_3.MemoExoticComponent<({ shapeId, type, text: plaintext, labelColor, fontFamily, fontSize, lineHeight, textAlign, verticalAlign, wrap, isSelected, padding, onKeyDown: handleKeyDownCustom, classNamePrefix, style, textWidth, textHeight, showTextOutline, }: PlainTextLabelProps) => JSX.Element | null>; + +// @public (undocumented) +export interface PlainTextLabelProps { + // (undocumented) + bounds?: Box; + // (undocumented) + classNamePrefix?: string; + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + isSelected: boolean; + // (undocumented) + labelColor: string; + // (undocumented) + lineHeight: number; + // (undocumented) + onKeyDown?(e: KeyboardEvent): void; + // (undocumented) + padding?: number; + // (undocumented) + shapeId: TLShapeId; + // (undocumented) + showTextOutline?: boolean; + // (undocumented) + style?: React_3.CSSProperties; + // (undocumented) + text?: string; + // (undocumented) + textAlign: 'center' | 'end' | 'start'; + // (undocumented) + textHeight?: number; + // (undocumented) + textWidth?: number; + // (undocumented) + type: ExtractShapeByProps<{ + text: string; + }>['type']; + // (undocumented) + verticalAlign: 'end' | 'middle' | 'start'; + // (undocumented) + wrap?: boolean; +} + +// @public (undocumented) +export const PORTRAIT_BREAKPOINT: { + readonly DESKTOP: 7; + readonly MOBILE_SM: 3; + readonly MOBILE_XS: 2; + readonly MOBILE_XXS: 1; + readonly MOBILE: 4; + readonly TABLET_SM: 5; + readonly TABLET: 6; + readonly ZERO: 0; +}; + +// @public (undocumented) +export type PORTRAIT_BREAKPOINT = (typeof PORTRAIT_BREAKPOINT)[keyof typeof PORTRAIT_BREAKPOINT]; + +// @public (undocumented) +export namespace PORTRAIT_BREAKPOINT { + // (undocumented) + export type DESKTOP = typeof PORTRAIT_BREAKPOINT.DESKTOP; + // (undocumented) + export type MOBILE = typeof PORTRAIT_BREAKPOINT.MOBILE; + // (undocumented) + export type MOBILE_SM = typeof PORTRAIT_BREAKPOINT.MOBILE_SM; + // (undocumented) + export type MOBILE_XS = typeof PORTRAIT_BREAKPOINT.MOBILE_XS; + // (undocumented) + export type MOBILE_XXS = typeof PORTRAIT_BREAKPOINT.MOBILE_XXS; + // (undocumented) + export type TABLET = typeof PORTRAIT_BREAKPOINT.TABLET; + // (undocumented) + export type TABLET_SM = typeof PORTRAIT_BREAKPOINT.TABLET_SM; + // (undocumented) + export type ZERO = typeof PORTRAIT_BREAKPOINT.ZERO; +} + +// @public (undocumented) +export function PreferencesGroup(): JSX.Element; + +// @public (undocumented) +export function preloadFont(id: string, font: TLTypeFace, targetDocument?: Document): Promise; + +// @public (undocumented) +export function PrintItem(): JSX.Element; + +// @public +export function putExcalidrawContent(editor: Editor, excalidrawClipboardContent: any, point?: VecLike): Promise; + +// @public (undocumented) +export function RectangleToolbarItem(): JSX.Element; + +// @public (undocumented) +export function registerDefaultExternalContentHandlers(editor: Editor, options: TLDefaultExternalContentHandlerOpts): void; + +// @public (undocumented) +export function registerDefaultSideEffects(editor: Editor): () => void; + +// @public +export function removeFrame(editor: Editor, ids: TLShapeId[]): void; + +// @public (undocumented) +export function RemoveFrameMenuItem(): JSX.Element | null; + +// @public +export function renderHtmlFromRichText(editor: Editor, richText: TLRichText_2): string; + +// @public +export function renderHtmlFromRichTextForMeasurement(editor: Editor, richText: TLRichText_2): string; + +// @public +export function renderHtmlFromRichTextWithExtensions(richText: TLRichText_2, extensions: Extensions): string; + +// @public +export function renderPlaintextFromRichText(editor: Editor, richText: TLRichText_2): string; + +// @public +export function renderRichTextFromHTML(editor: Editor, html: string): TLRichText_2; + +// @public (undocumented) +export function ReorderMenuItems(): JSX.Element; + +// @public (undocumented) +export function ReorderMenuSubmenu(): JSX.Element | null; + +// @public (undocumented) +export function RhombusToolbarItem(): JSX.Element; + +// @public +export const RichTextArea: React_3.ForwardRefExoticComponent>; + +// @public +export const RichTextLabel: React_3.MemoExoticComponent<({ shapeId, type, richText, labelColor, fontFamily, fontSize, lineHeight, textAlign, verticalAlign, wrap, isSelected, padding, onKeyDown: handleKeyDownCustom, classNamePrefix, style, textWidth, textHeight, hasCustomTabBehavior, showTextOutline, }: RichTextLabelProps) => JSX.Element | null>; + +// @public (undocumented) +export interface RichTextLabelProps { + // (undocumented) + bounds?: Box; + // (undocumented) + classNamePrefix?: string; + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + hasCustomTabBehavior?: boolean; + // (undocumented) + isSelected: boolean; + // (undocumented) + labelColor: string; + // (undocumented) + lineHeight: number; + // (undocumented) + onKeyDown?(e: KeyboardEvent): void; + // (undocumented) + padding?: number; + // (undocumented) + richText?: TLRichText_2; + // (undocumented) + shapeId: TLShapeId; + // (undocumented) + showTextOutline?: boolean; + // (undocumented) + style?: React_3.CSSProperties; + // (undocumented) + textAlign: 'center' | 'end' | 'start'; + // (undocumented) + textHeight?: number; + // (undocumented) + textWidth?: number; + // (undocumented) + type: ExtractShapeByProps<{ + richText: TLRichText_2; + }>['type']; + // (undocumented) + verticalAlign: 'end' | 'middle' | 'start'; + // (undocumented) + wrap?: boolean; +} + +// @public +export function RichTextSVG({ bounds, richText, fontSize, fontFamily, lineHeight, textAlign, verticalAlign, wrap, labelColor, padding, showTextOutline, }: RichTextSVGProps): JSX.Element; + +// @public (undocumented) +export interface RichTextSVGProps { + // (undocumented) + bounds: Box; + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + labelColor: string; + // (undocumented) + lineHeight: number; + // (undocumented) + padding: number; + // (undocumented) + richText: TLRichText_2; + // (undocumented) + showTextOutline?: boolean; + // (undocumented) + textAlign: 'center' | 'end' | 'start'; + // (undocumented) + verticalAlign: 'end' | 'middle' | 'start'; + // (undocumented) + wrap?: boolean; +} + +// @public (undocumented) +export function RotateCWMenuItem(): JSX.Element; + +// @public (undocumented) +export const RTL_LANGUAGES: Set; + +// @public +export function sanitizeSvg(svgText: string): string; + +// @public +export class ScribbleOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLScribbleOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + cacheSize: number; + streamline: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLScribbleOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export function SelectAllMenuItem(): JSX.Element; + +// @public +export class SelectionForegroundOverlayUtil extends OverlayUtil { + // (undocumented) + getCursor(overlay: TLSelectionForegroundOverlay): TLCursorType | undefined; + // (undocumented) + getGeometry(overlay: TLSelectionForegroundOverlay): Geometry2d | null; + // (undocumented) + getOverlays(): TLSelectionForegroundOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, _overlays: TLSelectionForegroundOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export class SelectTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + cleanUpDuplicateProps(): void; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + onEnter(): void; + // (undocumented) + onExit(): void; + // (undocumented) + reactor: (() => void) | undefined; +} + +// @public (undocumented) +export function SelectToolbarItem(): JSX.Element; + +// @public (undocumented) +export function serializeTldrawJson(editor: Editor): Promise; + +// @public (undocumented) +export function serializeTldrawJsonBlob(editor: Editor): Promise; + +// @internal (undocumented) +export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls): void; + +// @internal (undocumented) +export function setDefaultUiAssetUrls(urls: TLUiAssetUrls): void; + +// @public +export class ShapeHandleOverlayUtil extends OverlayUtil { + // (undocumented) + getCursor(_overlay: TLShapeHandleOverlay): TLCursorType | undefined; + // (undocumented) + getGeometry(overlay: TLShapeHandleOverlay): Geometry2d | null; + // (undocumented) + getOverlays(): TLShapeHandleOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLShapeHandleOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public +export class ShapeIndicatorOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLShapeIndicatorOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + hintedLineWidth: number; + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLShapeIndicatorOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export interface ShapeOptionsWithDisplayValues { + // (undocumented) + getCustomDisplayValues(editor: Editor, shape: Shape, theme: TLTheme, colorMode: 'dark' | 'light'): Partial; + // (undocumented) + getDefaultDisplayValues(editor: Editor, shape: Shape, theme: TLTheme, colorMode: 'dark' | 'light'): DisplayValues; +} + +// @public +export class SnapIndicatorOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLSnapIndicatorOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLSnapIndicatorOverlay[]): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export interface SolidPathBuilderOpts extends BasePathBuilderOpts { + // (undocumented) + style: 'solid'; +} + +// @internal (undocumented) +export function Spinner(props: React_3.SVGProps): JSX.Element; + +// @public (undocumented) +export function StackMenuItems(): JSX.Element; + +// @public +export function startEditingShapeWithRichText(editor: Editor, shapeOrId: TLShape | TLShapeId, options?: { + info?: TLEventInfo; + selectAll?: boolean; +}): void; + +// @public (undocumented) +export function StarToolbarItem(): JSX.Element; + +// @public +export interface StrokeOptions { + easing?(pressure: number): number; + end?: { + cap?: boolean; + easing?(distance: number): number; + taper?: boolean | number; + }; + last?: boolean; + simulatePressure?: boolean; + size?: number; + smoothing?: number; + start?: { + cap?: boolean; + easing?(distance: number): number; + taper?: boolean | number; + }; + // (undocumented) + streamline?: number; + thinning?: number; +} + +// @public +export interface StrokePoint { + // (undocumented) + distance: number; + // (undocumented) + input: Vec; + // (undocumented) + point: Vec; + // (undocumented) + pressure: number; + // (undocumented) + radius: number; + // (undocumented) + runningLength: number; +} + +// @public (undocumented) +export function StylePanelArrowheadPicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelArrowKindPicker(): JSX.Element | null; + +// @public (undocumented) +export const StylePanelButtonPicker: (props: StylePanelButtonPickerProps) => React.JSX.Element; + +// @public (undocumented) +export const StylePanelButtonPickerInline: (props: StylePanelButtonPickerProps) => React.JSX.Element; + +// @public (undocumented) +export interface StylePanelButtonPickerProps { + // (undocumented) + items: StyleValuesForUi; + // (undocumented) + onHistoryMark?(id: string): void; + // (undocumented) + onValueChange?(style: StyleProp, value: T): void; + // (undocumented) + style: StyleProp; + // (undocumented) + title: string; + // (undocumented) + uiType: string; + // (undocumented) + value: SharedStyle; +} + +// @public (undocumented) +export function StylePanelColorPicker(): JSX.Element | null; + +// @public (undocumented) +export interface StylePanelContext { + // (undocumented) + enhancedA11yMode: boolean; + // (undocumented) + onHistoryMark(id: string): void; + // (undocumented) + onOpacityChange(opacity: number): void; + // (undocumented) + onValueChange(style: StyleProp, value: T): void; + // (undocumented) + styles: ReadonlySharedStyleMap; +} + +// @public (undocumented) +export function StylePanelContextProvider({ children, styles }: StylePanelContextProviderProps): JSX.Element; + +// @public (undocumented) +export interface StylePanelContextProviderProps { + // (undocumented) + children: React.ReactNode; + // (undocumented) + styles: ReadonlySharedStyleMap; +} + +// @public (undocumented) +export function StylePanelDashPicker(): JSX.Element | null; + +// @public (undocumented) +export const StylePanelDoubleDropdownPicker: (props: StylePanelDoubleDropdownPickerProps) => React_2.JSX.Element; + +// @public (undocumented) +export const StylePanelDoubleDropdownPickerInline: (props: StylePanelDoubleDropdownPickerProps) => React_2.JSX.Element; + +// @public (undocumented) +export interface StylePanelDoubleDropdownPickerProps { + // (undocumented) + itemsA: StyleValuesForUi; + // (undocumented) + itemsB: StyleValuesForUi; + // (undocumented) + label: Exclude | TLUiTranslationKey; + // (undocumented) + labelA: Exclude | TLUiTranslationKey; + // (undocumented) + labelB: Exclude | TLUiTranslationKey; + // (undocumented) + onValueChange?(style: StyleProp, value: T): void; + // (undocumented) + styleA: StyleProp; + // (undocumented) + styleB: StyleProp; + // (undocumented) + uiTypeA: string; + // (undocumented) + uiTypeB: string; + // (undocumented) + valueA: SharedStyle; + // (undocumented) + valueB: SharedStyle; +} + +// @public (undocumented) +export const StylePanelDropdownPicker: (props: StylePanelDropdownPickerProps) => React_2.JSX.Element; + +// @public (undocumented) +export const StylePanelDropdownPickerInline: (props: StylePanelDropdownPickerProps) => React_2.JSX.Element; + +// @public (undocumented) +export interface StylePanelDropdownPickerProps { + // (undocumented) + id: string; + isOverflow?: boolean; + // (undocumented) + items: StyleValuesForUi; + // (undocumented) + label?: Exclude | TLUiTranslationKey; + // (undocumented) + onValueChange?(style: StyleProp, value: T): void; + sideOffset?: number; + // (undocumented) + style: StyleProp; + // (undocumented) + stylePanelType: string; + testIdType?: string; + // (undocumented) + type: 'icon' | 'menu' | 'tool'; + // (undocumented) + uiType: string; + // (undocumented) + value: SharedStyle; +} + +// @public (undocumented) +export function StylePanelFillPicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelFontPicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelGeoShapePicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelLabelAlignPicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelOpacityPicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelSection({ children }: StylePanelSectionProps): JSX.Element; + +// @public (undocumented) +export interface StylePanelSectionProps { + // (undocumented) + children: React_3.ReactNode; +} + +// @public (undocumented) +export function StylePanelSizePicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelSplinePicker(): JSX.Element | null; + +// @public (undocumented) +export function StylePanelSubheading({ children }: StylePanelSubheadingProps): JSX.Element; + +// @public (undocumented) +export interface StylePanelSubheadingProps { + // (undocumented) + children: React.ReactNode; +} + +// @public (undocumented) +export function StylePanelTextAlignPicker(): JSX.Element | null; + +// @public (undocumented) +export type StyleValuesForUi = readonly { + readonly icon: string | TLUiIconJsx; + readonly value: T; +}[]; + +export { TaskItem } + +export { TaskItemOptions } + +export { TaskList } + +export { TaskListOptions } + +// @public (undocumented) +export interface TextAreaProps { + // (undocumented) + handleBlur(): void; + // (undocumented) + handleChange(changeInfo: { + plaintext?: string; + richText?: TLRichText_2; + }): void; + // (undocumented) + handleDoubleClick(e: any): any; + // (undocumented) + handleFocus(): void; + // (undocumented) + handleInputPointerDown(e: React_3.PointerEvent): void; + // (undocumented) + handleKeyDown(e: KeyboardEvent): void; + // (undocumented) + handlePaste(e: ClipboardEvent | React_3.ClipboardEvent): void; + // (undocumented) + hasCustomTabBehavior?: boolean; + // (undocumented) + isEditing: boolean; + // (undocumented) + richText?: TLRichText_2; + // (undocumented) + shapeId: TLShapeId; + // (undocumented) + text?: string; +} + +// @public (undocumented) +export interface TextShapeOptions extends ShapeOptionsWithDisplayValues { + extraArrowHorizontalPadding: number; + showTextOutline: boolean; +} + +// @public (undocumented) +export class TextShapeTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + static initial: string; + // (undocumented) + shapeType: string; +} + +// @public (undocumented) +export class TextShapeUtil extends ShapeUtil { + // (undocumented) + canEdit(shape: TLTextShape): boolean; + // (undocumented) + component(shape: TLTextShape): JSX.Element; + // (undocumented) + getDefaultProps(): TLTextShape['props']; + // (undocumented) + getFontFaces(shape: TLTextShape): TLFontFace[]; + // (undocumented) + getGeometry(shape: TLTextShape, opts: TLGeometryOpts): Rectangle2d; + // (undocumented) + getIndicatorPath(shape: TLTextShape): Path2D | undefined; + // (undocumented) + getMinDimensions(shape: TLTextShape): { + height: number; + width: number; + }; + // (undocumented) + getText(shape: TLTextShape): string; + // (undocumented) + isAspectRatioLocked(shape: TLTextShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + onBeforeUpdate(prev: TLTextShape, next: TLTextShape): { + id: TLShapeId; + index: IndexKey; + isLocked: boolean; + meta: JsonObject; + opacity: TLOpacityType; + parentId: TLParentId; + props: { + autoSize: boolean; + color: TLDefaultColorStyle; + font: TLDefaultFontStyle; + richText: TLRichText; + scale: number; + size: TLDefaultSizeStyle; + textAlign: TLDefaultTextAlignStyle; + w: number; + }; + rotation: number; + type: "text"; + typeName: 'shape'; + x: number; + y: number; + } | undefined; + // (undocumented) + onEditEnd(shape: TLTextShape): void; + // (undocumented) + onResize(shape: TLTextShape, info: TLResizeInfo): { + id: TLShapeId; + props: { + autoSize: boolean; + w: number; + }; + type: "text"; + x: number; + y: number; + } | { + id: TLShapeId; + props: { + scale: number; + }; + type: "text"; + x: number; + y: number; + }; + // (undocumented) + options: TextShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLTextShape, ctx: SvgExportContext): JSX.Element; + // (undocumented) + static type: 'text'; +} + +// @public (undocumented) +export interface TextShapeUtilDisplayValues { + // (undocumented) + color: string; + // (undocumented) + fontFamily: string; + // (undocumented) + fontSize: number; + // (undocumented) + fontStyle: string; + // (undocumented) + fontVariant: string; + // (undocumented) + fontWeight: string; + // (undocumented) + lineHeight: number; +} + +// @public (undocumented) +export function TextToolbarItem(): JSX.Element; + +// @public +export const tipTapDefaultExtensions: Extensions; + +// @public (undocumented) +export interface TLArcArrowInfo { + // (undocumented) + bindings: TLArrowBindings; + // (undocumented) + bodyArc: TLArcInfo; + // (undocumented) + end: TLArrowPoint; + // (undocumented) + handleArc: TLArcInfo; + // (undocumented) + isValid: boolean; + // (undocumented) + middle: VecLike; + // (undocumented) + start: TLArrowPoint; + // (undocumented) + type: 'arc'; +} + +// @public (undocumented) +export interface TLArcInfo { + // (undocumented) + center: VecLike; + // (undocumented) + largeArcFlag: number; + // (undocumented) + length: number; + // (undocumented) + radius: number; + // (undocumented) + size: number; + // (undocumented) + sweepFlag: number; +} + +// @public (undocumented) +export interface TLArrowBindingHintOverlay extends TLOverlay { + // (undocumented) + props: { + arrowId: TLShapeId; + }; +} + +// @public (undocumented) +export interface TLArrowBindings { + // (undocumented) + end: TLArrowBinding | undefined; + // (undocumented) + start: TLArrowBinding | undefined; +} + +// @public (undocumented) +export interface TLArrowHintOverlay extends TLOverlay { + // (undocumented) + props: { + anchorX: number; + anchorY: number; + arrowKind: string; + handles: { + bottom: { + isEnabled: boolean; + x: number; + y: number; + }; + left: { + isEnabled: boolean; + x: number; + y: number; + }; + right: { + isEnabled: boolean; + x: number; + y: number; + }; + top: { + isEnabled: boolean; + x: number; + y: number; + }; + }; + isExact: boolean; + isPrecise: boolean; + showEdgeHints: boolean; + snap: string; + targetId: TLShapeId; + }; +} + +// @public (undocumented) +export type TLArrowInfo = TLArcArrowInfo | TLElbowArrowInfo | TLStraightArrowInfo; + +// @public (undocumented) +export interface TLArrowPoint { + // (undocumented) + arrowhead: TLArrowShapeArrowheadStyle; + // (undocumented) + handle: VecLike; + // (undocumented) + point: VecLike; +} + +// @public (undocumented) +export interface TLBrushOverlay extends TLOverlay { + // (undocumented) + props: { + h: number; + w: number; + x: number; + y: number; + }; +} + +// @public (undocumented) +export interface TLCollaboratorBrushOverlay extends TLOverlay { + // (undocumented) + props: { + color: string; + h: number; + w: number; + x: number; + y: number; + }; +} + +// @public @deprecated (undocumented) +export interface TLCollaboratorCursorOverlay extends TLOverlay { + // (undocumented) + props: { + chatMessage: string; + color: string; + name: null | string; + x: number; + y: number; + }; +} + +// @public (undocumented) +export interface TLCollaboratorHintOverlay extends TLOverlay { + // (undocumented) + props: { + x: number; + rotation: number; + color: string; + y: number; + }; +} + +// @public (undocumented) +export interface TLCollaboratorScribbleOverlay extends TLOverlay { + // (undocumented) + props: { + color: string; + scribble: TLScribble; + }; +} + +// @public (undocumented) +export interface TLCollaboratorShapeIndicatorOverlay extends TLOverlay { + // (undocumented) + props: { + indicators: Array<{ + color: string; + shapeIds: TLShapeId[]; + }>; + }; +} + +// @public +export interface TLComponents extends TLEditorComponents, TLUiComponents { +} + +// @public (undocumented) +export type TLCopyType = 'json' | 'png' | 'svg'; + +// @public (undocumented) +export interface TLDefaultExternalContentHandlerOpts extends TLExternalContentProps { + // (undocumented) + msg: ReturnType; + // (undocumented) + toasts: TLUiToastsContextType; +} + +// @public (undocumented) +export interface TLDefaultFont { + // (undocumented) + italic: { + bold: TLFontFace_2; + normal: TLFontFace_2; + }; + // (undocumented) + normal: { + bold: TLFontFace_2; + normal: TLFontFace_2; + }; +} + +// @public (undocumented) +export interface TLDefaultFonts { + // (undocumented) + tldraw_draw: TLDefaultFont; + // (undocumented) + tldraw_mono: TLDefaultFont; + // (undocumented) + tldraw_sans: TLDefaultFont; + // (undocumented) + tldraw_serif: TLDefaultFont; +} + +// @public (undocumented) +export function Tldraw(props: TldrawProps): JSX.Element; + +// @public (undocumented) +export const TLDRAW_FILE_EXTENSION: '.tldr'; + +// @public (undocumented) +export interface TldrawBaseProps extends TldrawUiProps, TldrawEditorBaseProps, TLExternalContentProps { + assetUrls?: TLUiAssetUrlOverrides; + components?: TLComponents; + // @deprecated + embeds?: TLEmbedDefinition[]; + locale?: string; + // @deprecated + textOptions?: TLTextOptions; +} + +// @public (undocumented) +export interface TldrawFile { + // (undocumented) + records: UnknownRecord[]; + // (undocumented) + schema: SerializedSchema; + // (undocumented) + tldrawFileFormatVersion: number; +} + +// @public (undocumented) +export type TldrawFileParseError = { + cause: unknown; + type: 'invalidRecords'; +} | { + cause: unknown; + type: 'notATldrawFile'; +} | { + data: any; + type: 'v1File'; +} | { + reason: MigrationFailureReason; + type: 'migrationFailed'; +} | { + type: 'fileFormatVersionTooNew'; + version: number; +}; + +// @public +export const TldrawImage: MemoExoticComponent<(props: TldrawImageProps) => JSX.Element>; + +// @public (undocumented) +export interface TldrawImageProps extends TLImageExportOptions { + assets?: TLAssetStore; + assetUrls?: TLUiAssetUrlOverrides; + bindingUtils?: readonly TLAnyBindingUtilConstructor[]; + format?: 'png' | 'svg'; + licenseKey?: string; + options?: Partial; + pageId?: TLPageId; + shapeUtils?: readonly TLAnyShapeUtilConstructor[]; + snapshot: Partial | TLStoreSnapshot; + // @deprecated + textOptions?: TLTextOptions; +} + +// @public (undocumented) +export type TldrawProps = TldrawBaseProps & TldrawEditorStoreProps; + +// @public (undocumented) +export const TldrawUi: React_3.MemoExoticComponent<({ renderDebugMenuItems, children, hideUi, components, ...rest }: TldrawUiProps) => JSX.Element>; + +// @public (undocumented) +export function TldrawUiA11yProvider({ children }: A11yProviderProps): JSX.Element; + +// @public (undocumented) +export const TldrawUiButton: React_2.ForwardRefExoticComponent>; + +// @public (undocumented) +export function TldrawUiButtonCheck({ checked }: TLUiButtonCheckProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiButtonIcon({ icon, small, invertIcon }: TLUiButtonIconProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiButtonLabel({ children }: TLUiButtonLabelProps): JSX.Element; + +// @public +export const TldrawUiColumn: ForwardRefExoticComponent>; + +// @public (undocumented) +export function TldrawUiComponentsProvider({ overrides, children, }: TLUiComponentsProviderProps): JSX.Element; + +// @public (undocumented) +export const TldrawUiContextProvider: NamedExoticComponent; + +// @public +export function TldrawUiContextualToolbar({ children, className, isMousingDown, getSelectionBounds, changeOnlyWhenYChanges, label, }: TLUiContextualToolbarProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDialogBody({ className, children, style }: TLUiDialogBodyProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDialogCloseButton(): JSX.Element; + +// @public (undocumented) +export function TldrawUiDialogFooter({ className, children }: TLUiDialogFooterProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDialogHeader({ className, children }: TLUiDialogHeaderProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDialogsProvider({ context, children }: TLUiDialogsProviderProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDialogTitle({ className, children, style }: TLUiDialogTitleProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuCheckboxItem({ children, onSelect, ...rest }: TLUiDropdownMenuCheckboxItemProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuContent({ className, side, align, sideOffset, alignOffset, collisionPadding, children, }: TLUiDropdownMenuContentProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuGroup({ className, children }: TLUiDropdownMenuGroupProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuIndicator(): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuItem({ noClose, children }: TLUiDropdownMenuItemProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuRoot({ id, children, modal, debugOpen, }: TLUiDropdownMenuRootProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuSubContent({ id, alignOffset, sideOffset, size, children, }: TLUiDropdownMenuSubContentProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuSubTrigger({ id, label, title, disabled, }: TLUiDropdownMenuSubTriggerProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiDropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiEventsProvider({ onEvent, children }: EventsProviderProps): JSX.Element; + +// @public +export const TldrawUiGrid: ForwardRefExoticComponent>; + +// @public (undocumented) +export const TldrawUiIcon: MemoExoticComponent<({ label, small, invertIcon, icon, tiny, color, className, ...props }: TLUiIconProps) => JSX.Element>; + +// @public (undocumented) +export function TldrawUiInFrontOfTheCanvas(): JSX.Element; + +// @public (undocumented) +export const TldrawUiInput: React_2.ForwardRefExoticComponent>; + +// @public (undocumented) +export function TldrawUiKbd({ children, visibleOnMobileLayout }: TLUiKbdProps): JSX.Element | null; + +// @public (undocumented) +export function TldrawUiMenuActionCheckboxItem({ actionId, ...rest }: TLUiMenuActionCheckboxItemProps): JSX.Element | null; + +// @public (undocumented) +export function TldrawUiMenuActionItem({ actionId, ...rest }: TLUiMenuActionItemProps): JSX.Element | null; + +// @public (undocumented) +export function TldrawUiMenuCheckboxItem({ id, kbd, label, lang, readonlyOk, onSelect, toggle, disabled, checked, }: TLUiMenuCheckboxItemProps): JSX.Element | null; + +// @public (undocumented) +export function TldrawUiMenuContextProvider({ type, sourceId, children, }: TLUiMenuContextProviderProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiMenuGroup({ id, label, className, children }: TLUiMenuGroupProps): bigint | boolean | JSX.Element | Iterable | null | number | Promise | string> | ReactPortal | Iterable | null | number | string | undefined> | string | undefined; + +// @public (undocumented) +export function TldrawUiMenuItem({ disabled, spinner, readonlyOk, id, kbd, label, icon, iconLeft, onSelect, noClose, isSelected, onDragStart, }: TLUiMenuItemProps): JSX.Element | null; + +// @public (undocumented) +export function TldrawUiMenuSubmenu({ id, disabled, label, size, children, }: TLUiMenuSubmenuProps): bigint | boolean | JSX.Element | Iterable | null | number | Promise | string> | ReactPortal | Iterable | null | number | string | undefined> | string | undefined; + +// @public (undocumented) +export function TldrawUiMenuToolItem({ toolId, ...rest }: TLUiMenuToolItemProps): JSX.Element | null; + +// @public (undocumented) +export interface TldrawUiOrientationContext { + // (undocumented) + orientation: 'horizontal' | 'vertical'; + // (undocumented) + tooltipSide: 'bottom' | 'left' | 'right' | 'top'; +} + +// @public (undocumented) +export function TldrawUiOrientationProvider({ children, orientation, tooltipSide, }: TldrawUiOrientationProviderProps): JSX.Element; + +// @public (undocumented) +export interface TldrawUiOrientationProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + orientation: 'horizontal' | 'vertical'; + // (undocumented) + tooltipSide?: 'bottom' | 'left' | 'right' | 'top'; +} + +// @public (undocumented) +export function TldrawUiPopover({ id, children, onOpenChange, open, className }: TLUiPopoverProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiPopoverContent({ side, children, align, sideOffset, alignOffset, collisionPadding, disableEscapeKeyDown, autoFocusFirstButton, }: TLUiPopoverContentProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiPopoverTrigger({ children }: TLUiPopoverTriggerProps): JSX.Element; + +// @public (undocumented) +export interface TldrawUiProps extends TLUiContextProviderProps { + assetUrls?: TLUiAssetUrlOverrides; + children?: ReactNode; + components?: TLUiComponents; + hideUi?: boolean; + renderDebugMenuItems?(): React_3.ReactNode; +} + +// @public +export const TldrawUiRow: ForwardRefExoticComponent>; + +// @public +export function TldrawUiSelect({ id, value, onValueChange, onOpenChange, disabled, className, children, 'data-testid': dataTestId, 'aria-label': ariaLabel, }: TLUiSelectProps): JSX.Element; + +// @public +export function TldrawUiSelectContent({ children, side, align, className, }: TLUiSelectContentProps): JSX.Element; + +// @public +export function TldrawUiSelectItem({ value, label, icon, disabled, className, }: TLUiSelectItemProps): JSX.Element; + +// @public +export const TldrawUiSelectTrigger: React_2.ForwardRefExoticComponent>; + +// @public +export function TldrawUiSelectValue({ placeholder, icon, children }: TLUiSelectValueProps): JSX.Element; + +// @public (undocumented) +export const TldrawUiSlider: React_3.ForwardRefExoticComponent>; + +// @public (undocumented) +export function TldrawUiToastsProvider({ children }: TLUiToastsProviderProps): JSX.Element; + +// @public (undocumented) +export const TldrawUiToolbar: React_3.ForwardRefExoticComponent>; + +// @public (undocumented) +export const TldrawUiToolbarButton: React_3.ForwardRefExoticComponent>; + +// @public (undocumented) +export function TldrawUiToolbarToggleGroup({ children, className, type, asChild, ...props }: TLUiToolbarToggleGroupProps): JSX.Element; + +// @public (undocumented) +export function TldrawUiToolbarToggleItem({ children, className, type, value, tooltip, ...props }: TLUiToolbarToggleItemProps): JSX.Element; + +// @public (undocumented) +export const TldrawUiTooltip: React_3.ForwardRefExoticComponent>; + +// @public (undocumented) +export interface TldrawUiTooltipProps { + // (undocumented) + children: React_3.ReactNode; + // (undocumented) + content?: React_3.ReactNode | string; + // (undocumented) + delayDuration?: number; + // (undocumented) + disabled?: boolean; + // (undocumented) + showOnMobile?: boolean; + // (undocumented) + side?: 'bottom' | 'left' | 'right' | 'top'; + // (undocumented) + sideOffset?: number; +} + +// @public (undocumented) +export function TldrawUiTooltipProvider({ children }: TldrawUiTooltipProviderProps): JSX.Element; + +// @public (undocumented) +export interface TldrawUiTooltipProviderProps { + // (undocumented) + children: React_3.ReactNode; +} + +// @public +export function TldrawUiTranslationProvider({ overrides, locale, children, }: TLUiTranslationProviderProps): JSX.Element; + +// @public (undocumented) +export interface TLEditorAssetUrls { + // (undocumented) + fonts?: { + [key: string]: string | undefined; + tldraw_draw_bold?: string; + tldraw_draw_italic_bold?: string; + tldraw_draw_italic?: string; + tldraw_draw?: string; + tldraw_mono_bold?: string; + tldraw_mono_italic_bold?: string; + tldraw_mono_italic?: string; + tldraw_mono?: string; + tldraw_sans_bold?: string; + tldraw_sans_italic_bold?: string; + tldraw_sans_italic?: string; + tldraw_sans?: string; + tldraw_serif_bold?: string; + tldraw_serif_italic_bold?: string; + tldraw_serif_italic?: string; + tldraw_serif?: string; + }; +} + +// @public (undocumented) +export interface TLElbowArrowInfo { + // (undocumented) + bindings: TLArrowBindings; + // (undocumented) + elbow: ElbowArrowInfo; + // (undocumented) + end: TLArrowPoint; + // (undocumented) + isValid: boolean; + // (undocumented) + route: ElbowArrowRoute; + // (undocumented) + start: TLArrowPoint; + // (undocumented) + type: 'elbow'; +} + +// @public (undocumented) +export type TLEmbedDefinition = CustomEmbedDefinition | EmbedDefinition; + +// @public (undocumented) +export type TLEmbedResult = { + definition: TLEmbedDefinition; + embedUrl: string; + url: string; +} | undefined; + +// @public (undocumented) +export type TLEmbedShapePermissions = { + [K in keyof typeof embedShapePermissionDefaults]?: boolean; +}; + +// @public (undocumented) +export interface TLExternalContentProps { + acceptedImageMimeTypes?: readonly string[]; + acceptedVideoMimeTypes?: readonly string[]; + maxAssetSize?: number; + maxImageDimension?: number; +} + +// @public +export type TLNoteShapeAttributionComponent = ComponentType | null; + +// @public (undocumented) +export interface TLNoteShapeAttributionProps { + color: string; + firstName: string; + name: string; + scale: number; + shape: TLNoteShape; + variant: 'canvas' | 'export'; +} + +// @public (undocumented) +export interface TLScribbleOverlay extends TLOverlay { + // (undocumented) + props: { + scribble: TLScribble; + }; +} + +// @public (undocumented) +export interface TLSelectionForegroundOverlay extends TLOverlay { + // (undocumented) + props: { + handle: RotateCorner | TLSelectionHandle; + overlayType: 'mobile_rotate' | 'resize_handle' | 'rotate_handle'; + }; +} + +// @public (undocumented) +export interface TLShapeHandleOverlay extends TLOverlay { + // (undocumented) + props: { + handle: TLHandle; + shapeId: TLShapeId; + }; +} + +// @public (undocumented) +export interface TLShapeIndicatorOverlay extends TLOverlay { + // (undocumented) + props: { + hintingShapeIds: TLShapeId_2[]; + idsToDisplay: TLShapeId_2[]; + }; +} + +// @public (undocumented) +export interface TLSnapIndicatorOverlay extends TLOverlay { + // (undocumented) + props: { + line: SnapIndicator; + }; +} + +// @public (undocumented) +export interface TLStraightArrowInfo { + // (undocumented) + bindings: TLArrowBindings; + // (undocumented) + end: TLArrowPoint; + // (undocumented) + isValid: boolean; + // (undocumented) + length: number; + // (undocumented) + middle: VecLike; + // (undocumented) + start: TLArrowPoint; + // (undocumented) + type: 'straight'; +} + +// @public (undocumented) +export interface TLTypeFace { + // (undocumented) + display?: any; + // (undocumented) + featureSettings?: string; + // (undocumented) + format?: string; + // (undocumented) + stretch?: string; + // (undocumented) + style?: string; + // (undocumented) + unicodeRange?: string; + // (undocumented) + url: string; + // (undocumented) + variant?: string; + // (undocumented) + weight?: string; +} + +// @public (undocumented) +export interface TLUiA11y { + // (undocumented) + msg: string | undefined; + // (undocumented) + priority?: A11yPriority; +} + +// @public (undocumented) +export interface TLUiA11yContextType { + // (undocumented) + announce(msg: TLUiA11y): void; + // (undocumented) + currentMsg: Atom; +} + +// @public (undocumented) +export interface TLUiActionItem { + // (undocumented) + checkbox?: boolean; + // (undocumented) + icon?: IconType | React_2.ReactElement; + // (undocumented) + id: string; + // (undocumented) + isRequiredA11yAction?: boolean; + // (undocumented) + kbd?: string; + // (undocumented) + label?: { + [key: string]: TransationKey; + } | TransationKey; + // (undocumented) + onSelect(source: TLUiEventSource): Promise | void; + // (undocumented) + readonlyOk?: boolean; +} + +// @public (undocumented) +export type TLUiActionsContextType = Record; + +// @public (undocumented) +export interface TLUiActionsMenuProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export type TLUiAssetUrlOverrides = RecursivePartial; + +// @public (undocumented) +export interface TLUiAssetUrls extends TLEditorAssetUrls { + // (undocumented) + embedIcons: Partial>; + // (undocumented) + icons: Record | TLUiIconType, string>; + // (undocumented) + translations: Record<(typeof LANGUAGES)[number]['locale'], string>; +} + +// @public (undocumented) +export interface TLUiButtonCheckProps { + // (undocumented) + checked: boolean; +} + +// @public (undocumented) +export interface TLUiButtonIconProps { + // (undocumented) + icon: string | TLUiIconJsx; + // (undocumented) + invertIcon?: boolean; + // (undocumented) + small?: boolean; +} + +// @public (undocumented) +export interface TLUiButtonLabelProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export interface TLUiButtonProps extends React_2.HTMLAttributes { + // (undocumented) + disabled?: boolean; + // (undocumented) + htmlButtonType?: 'button' | 'reset' | 'submit'; + // (undocumented) + isActive?: boolean; + // (undocumented) + tooltip?: string; + // (undocumented) + type: 'danger' | 'help' | 'icon' | 'low' | 'menu' | 'normal' | 'primary' | 'tool'; +} + +// @public (undocumented) +export interface TLUiComponents { + // (undocumented) + A11y?: ComponentType | null; + // (undocumented) + ActionsMenu?: ComponentType | null; + // (undocumented) + ContextMenu?: ComponentType | null; + // (undocumented) + CursorChatBubble?: ComponentType | null; + // (undocumented) + DebugMenu?: ComponentType | null; + // (undocumented) + DebugPanel?: ComponentType | null; + // (undocumented) + Dialogs?: ComponentType | null; + // (undocumented) + FollowingIndicator?: ComponentType | null; + // (undocumented) + HelperButtons?: ComponentType | null; + // (undocumented) + HelpMenu?: ComponentType | null; + // (undocumented) + ImageToolbar?: ComponentType | null; + // (undocumented) + KeyboardShortcutsDialog?: ComponentType | null; + // (undocumented) + MainMenu?: ComponentType | null; + // (undocumented) + MenuPanel?: ComponentType | null; + // (undocumented) + Minimap?: ComponentType | null; + // (undocumented) + NavigationPanel?: ComponentType | null; + // (undocumented) + PageMenu?: ComponentType | null; + // (undocumented) + PeopleMenu?: ComponentType | null; + // (undocumented) + PeopleMenuAvatar?: ComponentType | null; + // (undocumented) + PeopleMenuFacePile?: ComponentType | null; + // (undocumented) + PeopleMenuItem?: ComponentType | null; + // (undocumented) + QuickActions?: ComponentType | null; + // (undocumented) + RichTextToolbar?: ComponentType | null; + // (undocumented) + SharePanel?: ComponentType | null; + // (undocumented) + StylePanel?: ComponentType | null; + // (undocumented) + Toasts?: ComponentType | null; + // (undocumented) + Toolbar?: ComponentType | null; + // (undocumented) + TopPanel?: ComponentType | null; + // (undocumented) + UserPresenceEditor?: ComponentType | null; + // (undocumented) + VideoToolbar?: ComponentType | null; + // (undocumented) + ZoomMenu?: ComponentType | null; +} + +// @public (undocumented) +export interface TLUiComponentsProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + overrides?: TLUiComponents; +} + +// @public (undocumented) +export interface TLUiContextMenuProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + disabled?: boolean; +} + +// @public (undocumented) +export interface TLUiContextProviderProps { + assetUrls?: RecursivePartial; + children?: ReactNode; + components?: TLUiComponents; + forceMobile?: boolean; + mediaMimeTypes?: string[]; + onUiEvent?: TLUiEventHandler; + overrides?: TLUiOverrides | TLUiOverrides[]; +} + +// @public (undocumented) +export interface TLUiContextualToolbarProps { + // (undocumented) + changeOnlyWhenYChanges?: boolean; + // (undocumented) + children?: React_3.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + getSelectionBounds(): Box | undefined; + // (undocumented) + isMousingDown?: boolean; + // (undocumented) + label: string; +} + +// @public (undocumented) +export interface TLUiDebugMenuProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export interface TLUiDialog { + // (undocumented) + component: ComponentType; + // (undocumented) + id: string; + // (undocumented) + onClose?(): void; + // (undocumented) + preventBackgroundClose?: boolean; +} + +// @public (undocumented) +export interface TLUiDialogBodyProps { + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + style?: CSSProperties; +} + +// @public (undocumented) +export interface TLUiDialogFooterProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + className?: string; +} + +// @public (undocumented) +export interface TLUiDialogHeaderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; +} + +// @public (undocumented) +export interface TLUiDialogProps { + // (undocumented) + onClose(): void; +} + +// @public (undocumented) +export interface TLUiDialogsContextType { + // (undocumented) + addDialog(dialog: Omit & { + id?: string; + }): string; + // (undocumented) + clearDialogs(): void; + // (undocumented) + dialogs: Atom; + // (undocumented) + removeDialog(id: string): string; +} + +// @public (undocumented) +export interface TLUiDialogsProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + context?: string; + // (undocumented) + overrides?(editor: Editor): TLUiDialogsContextType; +} + +// @public (undocumented) +export interface TLUiDialogTitleProps { + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + style?: CSSProperties; +} + +// @public (undocumented) +export interface TLUiDropdownMenuCheckboxItemProps { + // (undocumented) + checked?: boolean; + // (undocumented) + children: ReactNode; + // (undocumented) + disabled?: boolean; + // (undocumented) + onSelect?(e: Event): void; + // (undocumented) + title: string; +} + +// @public (undocumented) +export interface TLUiDropdownMenuContentProps { + // (undocumented) + align?: 'center' | 'end' | 'start'; + // (undocumented) + alignOffset?: number; + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + collisionPadding?: number; + // (undocumented) + id?: string; + // (undocumented) + side?: 'bottom' | 'left' | 'right' | 'top'; + // (undocumented) + sideOffset?: number; +} + +// @public (undocumented) +export interface TLUiDropdownMenuGroupProps { + // (undocumented) + children: ReactNode; + // (undocumented) + className?: string; +} + +// @public (undocumented) +export interface TLUiDropdownMenuItemProps { + // (undocumented) + children: ReactNode; + // (undocumented) + noClose?: boolean; +} + +// @public (undocumented) +export interface TLUiDropdownMenuRootProps { + // (undocumented) + children: ReactNode; + // (undocumented) + debugOpen?: boolean; + // (undocumented) + id: string; + // (undocumented) + modal?: boolean; +} + +// @public (undocumented) +export interface TLUiDropdownMenuSubContentProps { + // (undocumented) + alignOffset?: number; + // (undocumented) + children: ReactNode; + // (undocumented) + id?: string; + // (undocumented) + sideOffset?: number; + // (undocumented) + size?: 'medium' | 'small' | 'tiny' | 'wide'; +} + +// @public (undocumented) +export interface TLUiDropdownMenuSubProps { + // (undocumented) + children: ReactNode; + // (undocumented) + id: string; +} + +// @public (undocumented) +export interface TLUiDropdownMenuSubTriggerProps { + // (undocumented) + disabled?: boolean; + // (undocumented) + id?: string; + // (undocumented) + label: string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export interface TLUiDropdownMenuTriggerProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export type TLUiEventContextType = TLUiEventHandler; + +// @public (undocumented) +export type TLUiEventData = K extends null ? { + source: TLUiEventSource; +} : { + source: TLUiEventSource; +} & K; + +// @public (undocumented) +export type TLUiEventHandler = (name: T, data: TLUiEventData) => void; + +// @public (undocumented) +export interface TLUiEventMap { + // (undocumented) + 'a11y-repeat-shape-announce': null; + // (undocumented) + 'adjust-shape-styles': null; + // (undocumented) + 'align-shapes': { + operation: 'bottom' | 'center-horizontal' | 'center-vertical' | 'left' | 'right' | 'top'; + }; + // (undocumented) + 'alt-text-start': null; + // (undocumented) + 'change-language': { + locale: string; + }; + // (undocumented) + 'change-page': { + direction?: 'next' | 'prev'; + }; + // (undocumented) + 'change-user-name': null; + // (undocumented) + 'close-menu': { + id: string; + }; + // (undocumented) + 'color-scheme': { + value: string; + }; + // (undocumented) + 'convert-to-bookmark': null; + // (undocumented) + 'convert-to-embed': null; + // (undocumented) + 'copy-as': { + format: 'json' | 'png' | 'svg'; + }; + // (undocumented) + 'copy-hovered-styles': null; + // (undocumented) + 'copy-link': null; + // (undocumented) + 'create-new-project': null; + // (undocumented) + 'delete-page': null; + // (undocumented) + 'delete-shapes': null; + // (undocumented) + 'distribute-shapes': { + operation: 'horizontal' | 'vertical'; + }; + // (undocumented) + 'download-original': null; + // (undocumented) + 'drag-tool': { + id: string; + }; + // (undocumented) + 'duplicate-page': null; + // (undocumented) + 'duplicate-shapes': null; + // (undocumented) + 'edit-link': null; + // (undocumented) + 'enhanced-a11y-mode': null; + // (undocumented) + 'enlarge-shapes': null; + // (undocumented) + 'exit-pen-mode': null; + // (undocumented) + 'export-all-as': { + format: 'json' | 'png' | 'svg'; + }; + // (undocumented) + 'export-as': { + format: 'json' | 'png' | 'svg'; + }; + // (undocumented) + 'fit-frame-to-content': null; + // (undocumented) + 'flatten-to-image': null; + // (undocumented) + 'flip-shapes': { + operation: 'horizontal' | 'vertical'; + }; + // (undocumented) + 'frame-selection': null; + // (undocumented) + 'group-shapes': null; + // (undocumented) + 'image-manipulate': null; + // (undocumented) + 'image-replace': null; + // (undocumented) + 'input-mode': { + value: string; + }; + // (undocumented) + 'insert-embed': null; + // (undocumented) + 'insert-media': null; + // (undocumented) + 'move-page': null; + // (undocumented) + 'move-to-new-page': null; + // (undocumented) + 'move-to-page': null; + // (undocumented) + 'new-page': null; + // (undocumented) + 'open-context-menu': null; + // (undocumented) + 'open-cursor-chat': null; + // (undocumented) + 'open-embed-link': null; + // (undocumented) + 'open-file': null; + // (undocumented) + 'open-kbd-shortcuts': null; + // (undocumented) + 'open-menu': { + id: string; + }; + // (undocumented) + 'open-url': { + destinationUrl: string; + }; + // (undocumented) + 'pack-shapes': null; + // (undocumented) + 'remove-frame': null; + // (undocumented) + 'rename-document': null; + // (undocumented) + 'rename-page': null; + // (undocumented) + 'reorder-shapes': { + operation: 'backward' | 'forward' | 'toBack' | 'toFront'; + }; + // (undocumented) + 'replace-media': null; + // (undocumented) + 'reset-zoom': null; + // (undocumented) + 'rich-text': { + operation: 'bold' | 'bulletList' | 'heading' | 'link-edit' | 'link-remove' | 'link-visit' | 'link' | 'strike'; + }; + // (undocumented) + 'rotate-ccw': { + fine: boolean; + }; + // (undocumented) + 'rotate-cw': { + fine: boolean; + }; + // (undocumented) + 'save-project-to-file': null; + // (undocumented) + 'select-adjacent-shape': { + direction: 'down' | 'left' | 'next' | 'prev' | 'right' | 'up'; + }; + // (undocumented) + 'select-all-shapes': null; + // (undocumented) + 'select-none-shapes': null; + // (undocumented) + 'select-tool': { + id: string; + }; + // (undocumented) + 'set-alt-text': null; + // (undocumented) + 'set-color': null; + // (undocumented) + 'set-style': { + id: string; + value: number | string; + }; + // (undocumented) + 'shrink-shapes': null; + // (undocumented) + 'stack-shapes': { + operation: 'horizontal' | 'vertical'; + }; + // (undocumented) + 'start-following': null; + // (undocumented) + 'stop-following': null; + // (undocumented) + 'stretch-shapes': { + operation: 'horizontal' | 'vertical'; + }; + // (undocumented) + 'toggle-auto-size': null; + // (undocumented) + 'toggle-debug-mode': null; + // (undocumented) + 'toggle-dynamic-size-mode': null; + // (undocumented) + 'toggle-edge-scrolling': null; + // (undocumented) + 'toggle-focus-mode': null; + // (undocumented) + 'toggle-grid-mode': null; + // (undocumented) + 'toggle-invert-zoom': null; + // (undocumented) + 'toggle-keyboard-shortcuts': null; + // (undocumented) + 'toggle-lock': null; + // (undocumented) + 'toggle-paste-at-cursor': null; + // (undocumented) + 'toggle-reduce-motion': null; + // (undocumented) + 'toggle-snap-mode': null; + // (undocumented) + 'toggle-tool-lock': null; + // (undocumented) + 'toggle-transparent': null; + // (undocumented) + 'toggle-wrap-mode': null; + // (undocumented) + 'ungroup-shapes': null; + // (undocumented) + 'unlock-all': null; + // (undocumented) + 'video-replace': null; + // (undocumented) + 'zoom-in': { + towardsCursor: boolean; + }; + // (undocumented) + 'zoom-into-view': null; + // (undocumented) + 'zoom-out': { + towardsCursor: boolean; + }; + // (undocumented) + 'zoom-to-content': null; + // (undocumented) + 'zoom-to-fit': null; + // (undocumented) + 'zoom-to-selection': null; + // (undocumented) + 'zoom-tool': null; + // (undocumented) + copy: null; + // (undocumented) + cut: null; + // (undocumented) + edit: null; + // (undocumented) + paste: null; + // (undocumented) + print: null; + // (undocumented) + redo: null; + // (undocumented) + undo: null; +} + +// @public (undocumented) +export type TLUiEventSource = 'actions-menu' | 'context-menu' | 'debug-panel' | 'dialog' | 'document-name' | 'export-menu' | 'help-menu' | 'helper-buttons' | 'image-toolbar' | 'kbd' | 'main-menu' | 'menu' | 'navigation-zone' | 'page-menu' | 'people-menu' | 'quick-actions' | 'rich-text-menu' | 'share-menu' | 'style-panel' | 'toolbar' | 'unknown' | 'video-toolbar' | 'zoom-menu'; + +// @public (undocumented) +export interface TLUiHelperButtonsProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export interface TLUiHelpMenuProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export type TLUiIconJsx = ReactElement>; + +// @public (undocumented) +export interface TLUiIconProps extends React.HTMLAttributes { + // (undocumented) + children?: undefined; + // (undocumented) + color?: string; + // (undocumented) + crossOrigin?: 'anonymous' | 'use-credentials'; + // (undocumented) + icon: Exclude | TLUiIconJsx | TLUiIconType; + // (undocumented) + invertIcon?: boolean; + // (undocumented) + label: string; + // (undocumented) + small?: boolean; + // (undocumented) + tiny?: boolean; +} + +// @public (undocumented) +export type TLUiIconType = 'align-bottom' | 'align-center-horizontal' | 'align-center-vertical' | 'align-left' | 'align-right' | 'align-top' | 'alt' | 'arrow-arc' | 'arrow-cycle' | 'arrow-elbow' | 'arrow-left' | 'arrowhead-arrow' | 'arrowhead-bar' | 'arrowhead-diamond' | 'arrowhead-dot' | 'arrowhead-none' | 'arrowhead-square' | 'arrowhead-triangle-inverted' | 'arrowhead-triangle' | 'blob' | 'bold' | 'bookmark' | 'bring-forward' | 'bring-to-front' | 'broken' | 'bulletList' | 'check-circle' | 'check' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'chevrons-ne' | 'chevrons-sw' | 'clipboard-copied' | 'clipboard-copy' | 'closed' | 'code' | 'color' | 'comment' | 'corners' | 'crop' | 'cross-2' | 'cross-circle' | 'dash-dashed' | 'dash-dotted' | 'dash-draw' | 'dash-solid' | 'disconnected' | 'discord' | 'distribute-horizontal' | 'distribute-vertical' | 'dot' | 'dots-horizontal' | 'dots-vertical' | 'download' | 'drag-handle-dots' | 'duplicate' | 'edit' | 'external-link' | 'fill-fill' | 'fill-lined-fill' | 'fill-none' | 'fill-pattern' | 'fill-semi' | 'fill-solid' | 'follow' | 'following' | 'font-draw' | 'font-mono' | 'font-sans' | 'font-serif' | 'geo-arrow-down' | 'geo-arrow-left' | 'geo-arrow-right' | 'geo-arrow-up' | 'geo-check-box' | 'geo-cloud' | 'geo-diamond' | 'geo-ellipse' | 'geo-heart' | 'geo-hexagon' | 'geo-octagon' | 'geo-oval' | 'geo-pentagon' | 'geo-rectangle' | 'geo-rhombus-2' | 'geo-rhombus' | 'geo-star' | 'geo-trapezoid' | 'geo-triangle' | 'geo-x-box' | 'github' | 'group' | 'heading' | 'help-circle' | 'highlight' | 'horizontal-align-end' | 'horizontal-align-middle' | 'horizontal-align-start' | 'info-circle' | 'italic' | 'leading' | 'link' | 'list' | 'lock' | 'manual' | 'menu' | 'minus' | 'mixed' | 'pack' | 'plus' | 'question-mark-circle' | 'question-mark' | 'redo' | 'reset-zoom' | 'rotate-ccw' | 'rotate-cw' | 'send-backward' | 'send-to-back' | 'share-1' | 'size-extra-large' | 'size-large' | 'size-medium' | 'size-small' | 'spline-cubic' | 'spline-line' | 'stack-horizontal' | 'stack-vertical' | 'status-offline' | 'stretch-horizontal' | 'stretch-vertical' | 'strike' | 'text-align-center' | 'text-align-left' | 'text-align-right' | 'toggle-off' | 'toggle-on' | 'tool-arrow' | 'tool-eraser' | 'tool-frame' | 'tool-hand' | 'tool-highlight' | 'tool-laser' | 'tool-line' | 'tool-media' | 'tool-note' | 'tool-pencil' | 'tool-pointer' | 'tool-screenshot' | 'tool-text' | 'trash' | 'twitter' | 'underline' | 'undo' | 'ungroup' | 'unlock' | 'vertical-align-end' | 'vertical-align-middle' | 'vertical-align-start' | 'warning-triangle' | 'zoom-in' | 'zoom-out'; + +// @public (undocumented) +export interface TLUiImageToolbarProps { + // (undocumented) + children?: React.ReactNode; +} + +// @public (undocumented) +export interface TLUiInputProps { + // (undocumented) + 'aria-label'?: string; + // (undocumented) + 'data-testid'?: string; + // (undocumented) + autoFocus?: boolean; + // (undocumented) + autoSelect?: boolean; + // (undocumented) + children?: React_2.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + defaultValue?: string; + // (undocumented) + disabled?: boolean; + // (undocumented) + icon?: Exclude | TLUiIconType; + // (undocumented) + iconLabel?: Exclude | TLUiTranslationKey; + // (undocumented) + iconLeft?: Exclude | TLUiIconType; + // (undocumented) + label?: Exclude | TLUiTranslationKey; + maxLength?: number; + // (undocumented) + onBlur?(value: string): void; + // (undocumented) + onCancel?(value: string): void; + // (undocumented) + onComplete?(value: string): void; + // (undocumented) + onFocus?(): void; + // (undocumented) + onValueChange?(value: string): void; + // (undocumented) + placeholder?: string; + shouldManuallyMaintainScrollPositionWhenFocused?: boolean; + // (undocumented) + value?: string; +} + +// @public (undocumented) +export interface TLUiKbdProps { + // (undocumented) + children: string; + // (undocumented) + visibleOnMobileLayout?: boolean; +} + +// @public (undocumented) +export type TLUiKeyboardShortcutsDialogProps = TLUiDialogProps & { + children?: ReactNode; +}; + +// @public (undocumented) +export interface TLUiLayoutProps extends HTMLAttributes { + // (undocumented) + asChild?: boolean; + // (undocumented) + children: ReactNode; + // (undocumented) + tooltipSide?: 'bottom' | 'left' | 'right' | 'top'; +} + +// @public (undocumented) +export interface TLUiMainMenuProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export type TLUiMenuActionCheckboxItemProps = { + actionId?: string; +} & Pick; + +// @public (undocumented) +export type TLUiMenuActionItemProps = { + actionId?: string; +} & Partial>; + +// @public (undocumented) +export interface TLUiMenuCheckboxItemProps { + // (undocumented) + checked?: boolean; + // (undocumented) + disabled?: boolean; + // (undocumented) + icon?: IconType | TLUiIconJsx; + // (undocumented) + id: string; + // (undocumented) + kbd?: string; + // (undocumented) + label?: { + [key: string]: TranslationKey; + } | TranslationKey; + // (undocumented) + lang?: string; + // (undocumented) + onSelect(source: TLUiEventSource): Promise | void; + // (undocumented) + readonlyOk?: boolean; + // (undocumented) + title?: string; + // (undocumented) + toggle?: boolean; +} + +// @public (undocumented) +export interface TLUiMenuContextProviderProps { + // (undocumented) + children: React.ReactNode; + // (undocumented) + sourceId: TLUiEventSource; + // (undocumented) + type: TLUiMenuContextType; +} + +// @public (undocumented) +export type TLUiMenuContextType = 'context-menu' | 'helper-buttons' | 'icons' | 'keyboard-shortcuts' | 'menu' | 'small-icons' | 'toolbar-overflow' | 'toolbar'; + +// @public (undocumented) +export interface TLUiMenuGroupProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + id: string; + label?: { + [key: string]: TranslationKey; + } | TranslationKey; +} + +// @public (undocumented) +export interface TLUiMenuItemProps { + disabled?: boolean; + icon?: IconType | TLUiIconJsx; + iconLeft?: IconType | TLUiIconJsx; + // (undocumented) + id: string; + isSelected?: boolean; + kbd?: string; + label?: { + [key: string]: TranslationKey; + } | TranslationKey; + noClose?: boolean; + onDragStart?(source: TLUiEventSource, info: TLPointerEventInfo): void; + onSelect(source: TLUiEventSource): Promise | void; + readonlyOk?: boolean; + spinner?: boolean; +} + +// @public (undocumented) +export interface TLUiMenuSubmenuProps { + // (undocumented) + children: ReactNode; + // (undocumented) + disabled?: boolean; + // (undocumented) + id: string; + // (undocumented) + label?: { + [key: string]: Translation; + } | Translation; + // (undocumented) + size?: 'medium' | 'small' | 'tiny' | 'wide'; +} + +// @public (undocumented) +export type TLUiMenuToolItemProps = { + toolId?: string; +} & Pick; + +// @public (undocumented) +export type TLUiOverrideHelpers = ReturnType; + +// @public (undocumented) +export interface TLUiOverrides { + // (undocumented) + actions?(editor: Editor, actions: TLUiActionsContextType, helpers: TLUiOverrideHelpers): TLUiActionsContextType; + // (undocumented) + tools?(editor: Editor, tools: TLUiToolsContextType, helpers: TLUiOverrideHelpers): TLUiToolsContextType; + // (undocumented) + translations?: TLUiTranslationProviderProps['overrides']; +} + +// @public (undocumented) +export interface TLUiPeopleMenuAvatarProps { + // (undocumented) + userId: TLUserId; +} + +// @public (undocumented) +export interface TLUiPeopleMenuFacePileProps { + // (undocumented) + userColor: string; + // (undocumented) + userIds: TLUserId[]; + // (undocumented) + userName: string; +} + +// @public (undocumented) +export interface TLUiPeopleMenuItemProps { + // (undocumented) + userId: TLUserId; +} + +// @public (undocumented) +export interface TLUiPopoverContentProps { + // (undocumented) + align?: 'center' | 'end' | 'start'; + // (undocumented) + alignOffset?: number; + // (undocumented) + autoFocusFirstButton?: boolean; + // (undocumented) + children: React_3.ReactNode; + collisionPadding?: number; + // (undocumented) + disableEscapeKeyDown?: boolean; + // (undocumented) + side: 'bottom' | 'left' | 'right' | 'top'; + // (undocumented) + sideOffset?: number; +} + +// @public (undocumented) +export interface TLUiPopoverProps { + // (undocumented) + children: React_3.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + id: string; + // (undocumented) + onOpenChange?(isOpen: boolean): void; + // (undocumented) + open?: boolean; +} + +// @public (undocumented) +export interface TLUiPopoverTriggerProps { + // (undocumented) + children?: React_3.ReactNode; +} + +// @public (undocumented) +export interface TLUiQuickActionsProps { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export interface TLUiRichTextToolbarProps { + // (undocumented) + children?: React_3.ReactNode; +} + +// @public (undocumented) +export interface TLUiSelectContentProps { + // (undocumented) + align?: 'center' | 'end' | 'start'; + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + side?: 'bottom' | 'top'; +} + +// @public (undocumented) +export interface TLUiSelectItemProps { + // (undocumented) + className?: string; + // (undocumented) + disabled?: boolean; + // (undocumented) + icon?: Exclude | TLUiIconType; + // (undocumented) + label: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface TLUiSelectProps { + // (undocumented) + 'aria-label'?: string; + // (undocumented) + 'data-testid'?: string; + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + disabled?: boolean; + // (undocumented) + id: string; + // (undocumented) + onOpenChange?(isOpen: boolean): void; + // (undocumented) + onValueChange(value: string): void; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface TLUiSelectTriggerProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + className?: string; +} + +// @public (undocumented) +export interface TLUiSelectValueProps { + // (undocumented) + children?: React_2.ReactNode; + // (undocumented) + icon?: Exclude | TLUiIconType; + // (undocumented) + placeholder?: string; +} + +// @public (undocumented) +export interface TLUiSliderProps { + // (undocumented) + 'data-testid'?: string; + // (undocumented) + ariaValueModifier?: number; + // (undocumented) + label: string; + // (undocumented) + min?: number; + // (undocumented) + onHistoryMark?(id: string): void; + // (undocumented) + onValueChange(value: number): void; + // (undocumented) + steps: number; + // (undocumented) + title: string; + // (undocumented) + value: null | number; +} + +// @public (undocumented) +export interface TLUiStylePanelProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + isMobile?: boolean; + // (undocumented) + styles?: null | ReadonlySharedStyleMap; +} + +// @public (undocumented) +export interface TLUiToast { + // (undocumented) + actions?: TLUiToastAction[]; + // (undocumented) + closeLabel?: string; + // (undocumented) + description?: string; + // (undocumented) + icon?: TLUiIconType; + // (undocumented) + iconLabel?: string; + // (undocumented) + id: string; + // (undocumented) + keepOpen?: boolean; + // (undocumented) + severity?: AlertSeverity; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export interface TLUiToastAction { + // (undocumented) + label: string; + // (undocumented) + onClick(): void; + // (undocumented) + type: 'danger' | 'normal' | 'primary'; +} + +// @public (undocumented) +export interface TLUiToastsContextType { + // (undocumented) + addToast(toast: Omit & { + id?: string; + }): string; + // (undocumented) + clearToasts(): void; + // (undocumented) + removeToast(id: TLUiToast['id']): string; + // (undocumented) + toasts: Atom; +} + +// @public (undocumented) +export interface TLUiToastsProviderProps { + // (undocumented) + children: ReactNode; + // (undocumented) + overrides?(editor: Editor): TLUiToastsContextType; +} + +// @public (undocumented) +export interface TLUiToolbarButtonProps extends React_3.HTMLAttributes { + // (undocumented) + asChild?: boolean; + // (undocumented) + children?: React_3.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + disabled?: boolean; + // (undocumented) + isActive?: boolean; + // (undocumented) + tooltip?: string; + // (undocumented) + type: 'icon' | 'menu' | 'tool'; +} + +// @public (undocumented) +export interface TLUiToolbarProps extends React_3.HTMLAttributes { + // (undocumented) + children?: React_3.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + dir?: 'ltr' | 'rtl'; + // (undocumented) + label: string; + // (undocumented) + orientation?: 'grid' | 'horizontal' | 'vertical'; + // (undocumented) + tooltipSide?: 'bottom' | 'left' | 'right' | 'top'; +} + +// @public (undocumented) +export interface TLUiToolbarToggleGroupProps extends React_3.HTMLAttributes { + // (undocumented) + asChild?: boolean; + // (undocumented) + children?: React_3.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + defaultValue?: any; + // (undocumented) + dir?: 'ltr' | 'rtl'; + // (undocumented) + type: 'multiple' | 'single'; + // (undocumented) + value: any; +} + +// @public (undocumented) +export interface TLUiToolbarToggleItemProps extends React_3.HTMLAttributes { + // (undocumented) + children?: React_3.ReactNode; + // (undocumented) + className?: string; + // (undocumented) + tooltip?: React_3.ReactNode; + // (undocumented) + type: 'icon' | 'tool'; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface TLUiToolItem { + // (undocumented) + icon: IconType | TLUiIconJsx; + // (undocumented) + id: string; + kbd?: string; + // (undocumented) + label: TranslationKey; + // (undocumented) + meta?: { + [key: string]: any; + }; + // (undocumented) + onDragStart?(source: TLUiEventSource, info: TLPointerEventInfo): void; + // (undocumented) + onSelect(source: TLUiEventSource): void; + // (undocumented) + readonlyOk?: boolean; + // (undocumented) + shortcutsLabel?: TranslationKey; +} + +// @public (undocumented) +export type TLUiToolsContextType = Record; + +// @public (undocumented) +export interface TLUiToolsProviderProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + overrides?(editor: Editor, tools: TLUiToolsContextType, helpers: Partial): TLUiToolsContextType; +} + +// @public (undocumented) +export interface TLUiTranslation { + // (undocumented) + readonly dir: 'ltr' | 'rtl'; + // (undocumented) + readonly label: string; + // (undocumented) + readonly locale: string; + // (undocumented) + readonly messages: Record; +} + +// @public (undocumented) +export type TLUiTranslationContextType = TLUiTranslation; + +// @public (undocumented) +export type TLUiTranslationKey = 'a11y.adjust-shape-styles' | 'a11y.enlarge-shape' | 'a11y.enter-leave-container' | 'a11y.move-shape-faster' | 'a11y.move-shape' | 'a11y.multiple-shapes' | 'a11y.open-context-menu' | 'a11y.open-keyboard-shortcuts' | 'a11y.pan-camera' | 'a11y.repeat-shape' | 'a11y.rotate-shape-ccw-fine' | 'a11y.rotate-shape-ccw' | 'a11y.rotate-shape-cw-fine' | 'a11y.rotate-shape-cw' | 'a11y.select-shape-direction' | 'a11y.select-shape' | 'a11y.shape-image' | 'a11y.shape-index' | 'a11y.shape-video' | 'a11y.shrink-shape' | 'a11y.skip-to-main-content' | 'a11y.status' | 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy-hovered-styles' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.download-original' | 'action.duplicate' | 'action.edit-link' | 'action.enhanced-a11y-mode.menu' | 'action.enhanced-a11y-mode' | 'action.exit-pen-mode' | 'action.export-all-as-png.short' | 'action.export-all-as-png' | 'action.export-all-as-svg.short' | 'action.export-all-as-svg' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flatten-to-image' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project-on-tldraw' | 'action.fork-project' | 'action.frame-selection' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.open-kbd-shortcuts' | 'action.pack' | 'action.paste-error-description' | 'action.paste-error-title' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rename' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.select-zoom-tool' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-none' | 'action.toggle-auto-pan' | 'action.toggle-auto-size' | 'action.toggle-auto-zoom' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-dynamic-size-mode.menu' | 'action.toggle-dynamic-size-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-invert-zoom.menu' | 'action.toggle-invert-zoom' | 'action.toggle-keyboard-shortcuts.menu' | 'action.toggle-keyboard-shortcuts' | 'action.toggle-lock' | 'action.toggle-mouse' | 'action.toggle-paste-at-cursor.menu' | 'action.toggle-paste-at-cursor' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-trackpad' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.toggle-wrap-mode.menu' | 'action.toggle-wrap-mode' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-quick' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'app.loading' | 'arrow-kind-style.arc' | 'arrow-kind-style.elbow' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'assets.files.amount-too-many' | 'assets.files.maximum-size' | 'assets.files.size-too-big' | 'assets.files.type-not-allowed' | 'assets.files.upload-failed' | 'assets.url.failed' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.white' | 'color-style.yellow' | 'comments.add-placeholder' | 'comments.add-reaction' | 'comments.cluster-label' | 'comments.copy-link' | 'comments.delete-comment' | 'comments.delete' | 'comments.dismiss' | 'comments.edit-comment' | 'comments.edit-placeholder' | 'comments.edit' | 'comments.empty-filtered' | 'comments.empty' | 'comments.filter' | 'comments.hide' | 'comments.link-copied' | 'comments.mention-no-matches' | 'comments.mention-you' | 'comments.more-options' | 'comments.only-my-comments' | 'comments.only-unread' | 'comments.pin-label-resolved' | 'comments.pin-label' | 'comments.preview-more' | 'comments.reacted-1' | 'comments.reacted-2' | 'comments.reacted-3' | 'comments.reacted-more-one' | 'comments.reacted-more' | 'comments.reopen' | 'comments.replies-one' | 'comments.replies' | 'comments.reply-placeholder' | 'comments.resolve' | 'comments.resolved-by' | 'comments.resolved' | 'comments.save' | 'comments.send' | 'comments.show-all-pages' | 'comments.show-resolved' | 'comments.show' | 'comments.stack-label' | 'comments.thread-title' | 'comments.title' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.edit' | 'context-menu.export-all-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context-menu.title' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'document-name-menu.copy-link' | 'document.default-name' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.external-link' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.fill' | 'fill-style.lined-fill' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.heart' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'handle.crop.bottom-left' | 'handle.crop.bottom-right' | 'handle.crop.bottom' | 'handle.crop.left' | 'handle.crop.right' | 'handle.crop.top-left' | 'handle.crop.top-right' | 'handle.crop.top' | 'handle.resize-bottom-left' | 'handle.resize-bottom-right' | 'handle.resize-bottom' | 'handle.resize-left' | 'handle.resize-right' | 'handle.resize-top-left' | 'handle.resize-top-right' | 'handle.resize-top' | 'handle.rotate.bottom_left_rotate' | 'handle.rotate.bottom_right_rotate' | 'handle.rotate.mobile_rotate' | 'handle.rotate.top_left_rotate' | 'handle.rotate.top_right_rotate' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.import-tldr-file' | 'help-menu.keyboard-shortcuts' | 'help-menu.privacy' | 'help-menu.terms' | 'help-menu.title' | 'help-menu.twitter' | 'menu.accessibility' | 'menu.color-theme' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.input-device' | 'menu.language' | 'menu.preferences' | 'menu.theme' | 'menu.title' | 'menu.view' | 'navigation-zone.minimap' | 'navigation-zone.title' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-pages-reached' | 'page-menu.new-page-initial-name' | 'page-menu.resize' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.anonymous-user' | 'people-menu.avatar-color' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'share-menu.copied' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.creating-project' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.a11y' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.disabled-notice' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.text-formatting' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'style-panel.align' | 'style-panel.arrow-kind' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.label-align' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.selected' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'theme.dark' | 'theme.light' | 'theme.system' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'toast.error' | 'toast.info' | 'toast.success' | 'toast.warning' | 'tool-panel.more' | 'tool-panel.title' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.aspect-ratio.circle' | 'tool.aspect-ratio.landscape' | 'tool.aspect-ratio.original' | 'tool.aspect-ratio.portrait' | 'tool.aspect-ratio.square' | 'tool.aspect-ratio.wide' | 'tool.aspect-ratio' | 'tool.bookmark' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.flip-horz' | 'tool.flip-vert' | 'tool.frame' | 'tool.hand' | 'tool.heart' | 'tool.hexagon' | 'tool.highlight' | 'tool.image-crop-confirm' | 'tool.image-crop' | 'tool.image-toolbar-title' | 'tool.image-zoom' | 'tool.laser' | 'tool.line' | 'tool.media-alt-text-confirm' | 'tool.media-alt-text-desc' | 'tool.media-alt-text' | 'tool.media' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.pointer-down' | 'tool.rectangle' | 'tool.replace-media' | 'tool.rhombus' | 'tool.rich-text-bold' | 'tool.rich-text-bulletList' | 'tool.rich-text-code' | 'tool.rich-text-header' | 'tool.rich-text-highlight' | 'tool.rich-text-italic' | 'tool.rich-text-link-remove' | 'tool.rich-text-link-visit' | 'tool.rich-text-link' | 'tool.rich-text-orderedList' | 'tool.rich-text-strikethrough' | 'tool.rich-text-toolbar-title' | 'tool.rotate-cw' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'ui.checked' | 'ui.close' | 'ui.unchecked' | 'verticalAlign-style.end' | 'verticalAlign-style.middle' | 'verticalAlign-style.start' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; + +// @public (undocumented) +export interface TLUiTranslationProviderProps { + // (undocumented) + children: React_2.ReactNode; + // (undocumented) + locale: string; + overrides?: Record>; +} + +// @public (undocumented) +export interface TLUiVideoToolbarProps { + // (undocumented) + children?: React.ReactNode; +} + +// @public (undocumented) +export interface TLUiZoomMenuProps { + // (undocumented) + children?: ReactNode; +} + +// @internal (undocumented) +export const TLV1AlignStyle: { + readonly End: 'end'; + readonly Justify: 'justify'; + readonly Middle: 'middle'; + readonly Start: 'start'; +}; + +// @internal (undocumented) +export type TLV1AlignStyle = (typeof TLV1AlignStyle)[keyof typeof TLV1AlignStyle]; + +// @internal (undocumented) +export interface TLV1ArrowBinding extends TLV1BaseBinding { + // (undocumented) + distance: number; + // (undocumented) + handleId: keyof TLV1ArrowShape['handles']; + // (undocumented) + point: number[]; +} + +// @internal (undocumented) +export interface TLV1ArrowShape extends TLV1BaseShape { + // (undocumented) + bend: number; + // (undocumented) + decorations?: { + end?: TLV1Decoration; + middle?: TLV1Decoration; + start?: TLV1Decoration; + }; + // (undocumented) + handles: { + bend: TLV1Handle; + end: TLV1Handle; + start: TLV1Handle; + }; + // (undocumented) + label?: string; + // (undocumented) + labelPoint?: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Arrow; +} + +// @internal (undocumented) +export type TLV1Asset = TLV1ImageAsset | TLV1VideoAsset; + +// @internal (undocumented) +export const TLV1AssetType: { + readonly Image: 'image'; + readonly Video: 'video'; +}; + +// @internal (undocumented) +export type TLV1AssetType = (typeof TLV1AssetType)[keyof typeof TLV1AssetType]; + +// @internal (undocumented) +export interface TLV1BaseAsset { + // (undocumented) + id: string; + // (undocumented) + type: string; +} + +// @internal (undocumented) +export interface TLV1BaseBinding { + // (undocumented) + fromId: string; + // (undocumented) + id: string; + // (undocumented) + toId: string; +} + +// @internal (undocumented) +export interface TLV1BaseShape { + // (undocumented) + assetId?: string; + // (undocumented) + childIndex: number; + // (undocumented) + children?: string[]; + // (undocumented) + handles?: Record; + // (undocumented) + id: string; + // (undocumented) + isAspectRatioLocked?: boolean; + // (undocumented) + isGenerated?: boolean; + // (undocumented) + isGhost?: boolean; + // (undocumented) + isHidden?: boolean; + // (undocumented) + isLocked?: boolean; + // (undocumented) + label?: string; + // (undocumented) + name: string; + // (undocumented) + parentId: string; + // (undocumented) + point: number[]; + // (undocumented) + rotation?: number; + // (undocumented) + style: TLV1ShapeStyles; + // (undocumented) + type: TLV1ShapeType; +} + +// @internal (undocumented) +export type TLV1Binding = TLV1ArrowBinding; + +// @internal (undocumented) +export interface TLV1Bounds { + // (undocumented) + height: number; + // (undocumented) + maxX: number; + // (undocumented) + maxY: number; + // (undocumented) + minX: number; + // (undocumented) + minY: number; + // (undocumented) + rotation?: number; + // (undocumented) + width: number; +} + +// @internal (undocumented) +export const TLV1ColorStyle: { + readonly Black: 'black'; + readonly Blue: 'blue'; + readonly Cyan: 'cyan'; + readonly Gray: 'gray'; + readonly Green: 'green'; + readonly Indigo: 'indigo'; + readonly LightGray: 'lightGray'; + readonly Orange: 'orange'; + readonly Red: 'red'; + readonly Violet: 'violet'; + readonly White: 'white'; + readonly Yellow: 'yellow'; +}; + +// @internal (undocumented) +export type TLV1ColorStyle = (typeof TLV1ColorStyle)[keyof typeof TLV1ColorStyle]; + +// @internal (undocumented) +export const TLV1DashStyle: { + readonly Dashed: 'dashed'; + readonly Dotted: 'dotted'; + readonly Draw: 'draw'; + readonly Solid: 'solid'; +}; + +// @internal (undocumented) +export type TLV1DashStyle = (typeof TLV1DashStyle)[keyof typeof TLV1DashStyle]; + +// @internal (undocumented) +export const TLV1Decoration: { + readonly Arrow: 'arrow'; +}; + +// @internal (undocumented) +export type TLV1Decoration = (typeof TLV1Decoration)[keyof typeof TLV1Decoration]; + +// @internal (undocumented) +export interface TLV1Document { + // (undocumented) + assets: Record; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + pages: Record; + // (undocumented) + pageStates: Record; + // (undocumented) + version: number; +} + +// @internal (undocumented) +export interface TLV1DrawShape extends TLV1BaseShape { + // (undocumented) + isComplete: boolean; + // (undocumented) + points: number[][]; + // (undocumented) + type: typeof TLV1ShapeType.Draw; +} + +// @internal (undocumented) +export interface TLV1EllipseShape extends TLV1BaseShape { + // (undocumented) + label?: string; + // (undocumented) + labelPoint?: number[]; + // (undocumented) + radius: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Ellipse; +} + +// @internal (undocumented) +export const TLV1FontStyle: { + readonly Mono: 'mono'; + readonly Sans: 'sans'; + readonly Script: 'script'; + readonly Serif: 'serif'; +}; + +// @internal (undocumented) +export type TLV1FontStyle = (typeof TLV1FontStyle)[keyof typeof TLV1FontStyle]; + +// @internal (undocumented) +export interface TLV1GroupShape extends TLV1BaseShape { + // (undocumented) + children: string[]; + // (undocumented) + size: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Group; +} + +// @internal (undocumented) +export interface TLV1Handle { + // (undocumented) + bindingId?: string; + // (undocumented) + canBind?: boolean; + // (undocumented) + id: string; + // (undocumented) + index: number; + // (undocumented) + point: number[]; +} + +// @internal (undocumented) +export interface TLV1ImageAsset extends TLV1BaseAsset { + // (undocumented) + fileName: string; + // (undocumented) + size: number[]; + // (undocumented) + src: string; + // (undocumented) + type: typeof TLV1AssetType.Image; +} + +// @internal (undocumented) +export interface TLV1ImageShape extends TLV1BaseShape { + // (undocumented) + assetId: string; + // (undocumented) + size: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Image; +} + +// @internal (undocumented) +export interface TLV1Page { + // (undocumented) + bindings: Record; + // (undocumented) + childIndex?: number; + // (undocumented) + id: string; + // (undocumented) + name?: string; + // (undocumented) + shapes: Record; +} + +// @internal (undocumented) +export interface TLV1PageState { + // (undocumented) + bindingId?: null | string; + // (undocumented) + brush?: null | TLV1Bounds; + // (undocumented) + camera: { + point: number[]; + zoom: number; + }; + // (undocumented) + editingId?: null | string; + // (undocumented) + hoveredId?: null | string; + // (undocumented) + id: string; + // (undocumented) + pointedId?: null | string; + // (undocumented) + selectedIds: string[]; +} + +// @internal (undocumented) +export interface TLV1RectangleShape extends TLV1BaseShape { + // (undocumented) + label?: string; + // (undocumented) + labelPoint?: number[]; + // (undocumented) + size: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Rectangle; +} + +// @internal (undocumented) +export type TLV1Shape = TLV1ArrowShape | TLV1DrawShape | TLV1EllipseShape | TLV1GroupShape | TLV1ImageShape | TLV1RectangleShape | TLV1StickyShape | TLV1TextShape | TLV1TriangleShape | TLV1VideoShape; + +// @internal (undocumented) +export interface TLV1ShapeStyles { + // (undocumented) + color: TLV1ColorStyle; + // (undocumented) + dash: TLV1DashStyle; + // (undocumented) + font?: TLV1FontStyle; + // (undocumented) + isFilled?: boolean; + // (undocumented) + scale?: number; + // (undocumented) + size: TLV1SizeStyle; + // (undocumented) + textAlign?: TLV1AlignStyle; +} + +// @internal (undocumented) +export const TLV1ShapeType: { + readonly Arrow: 'arrow'; + readonly Draw: 'draw'; + readonly Ellipse: 'ellipse'; + readonly Group: 'group'; + readonly Image: 'image'; + readonly Rectangle: 'rectangle'; + readonly Sticky: 'sticky'; + readonly Text: 'text'; + readonly Triangle: 'triangle'; + readonly Video: 'video'; +}; + +// @internal (undocumented) +export type TLV1ShapeType = (typeof TLV1ShapeType)[keyof typeof TLV1ShapeType]; + +// @internal (undocumented) +export const TLV1SizeStyle: { + readonly Large: 'large'; + readonly Medium: 'medium'; + readonly Small: 'small'; +}; + +// @internal (undocumented) +export type TLV1SizeStyle = (typeof TLV1SizeStyle)[keyof typeof TLV1SizeStyle]; + +// @internal (undocumented) +export interface TLV1StickyShape extends TLV1BaseShape { + // (undocumented) + size: number[]; + // (undocumented) + text: string; + // (undocumented) + type: typeof TLV1ShapeType.Sticky; +} + +// @internal (undocumented) +export interface TLV1TextShape extends TLV1BaseShape { + // (undocumented) + text: string; + // (undocumented) + type: typeof TLV1ShapeType.Text; +} + +// @internal (undocumented) +export interface TLV1TriangleShape extends TLV1BaseShape { + // (undocumented) + label?: string; + // (undocumented) + labelPoint?: number[]; + // (undocumented) + size: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Triangle; +} + +// @internal (undocumented) +export interface TLV1VideoAsset extends TLV1BaseAsset { + // (undocumented) + fileName: string; + // (undocumented) + size: number[]; + // (undocumented) + src: string; + // (undocumented) + type: typeof TLV1AssetType.Video; +} + +// @internal (undocumented) +export interface TLV1VideoShape extends TLV1BaseShape { + // (undocumented) + assetId: string; + // (undocumented) + currentTime: number; + // (undocumented) + isPlaying: boolean; + // (undocumented) + size: number[]; + // (undocumented) + type: typeof TLV1ShapeType.Video; +} + +// @public (undocumented) +export interface TLZoomBrushOverlay extends TLOverlay { + // (undocumented) + props: { + h: number; + w: number; + x: number; + y: number; + }; +} + +// @public (undocumented) +export function ToggleAutoSizeMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function ToggleDebugModeItem(): JSX.Element; + +// @public (undocumented) +export function ToggleDynamicSizeModeItem(): JSX.Element; + +// @public (undocumented) +export function ToggleEdgeScrollingItem(): JSX.Element; + +// @public (undocumented) +export function ToggleEnhancedA11yModeItem(): JSX.Element; + +// @public (undocumented) +export function ToggleFocusModeItem(): JSX.Element; + +// @public (undocumented) +export function ToggleGridItem(): JSX.Element; + +// @public (undocumented) +export function ToggleInvertZoomItem(): JSX.Element; + +// @public (undocumented) +export function ToggleKeyboardShortcutsItem(): JSX.Element; + +// @public (undocumented) +export function ToggleLockMenuItem(): JSX.Element | null; + +// @public (undocumented) +export function TogglePasteAtCursorItem(): JSX.Element; + +// @public (undocumented) +export function ToggleReduceMotionItem(): JSX.Element; + +// @public (undocumented) +export function ToggleSnapModeItem(): JSX.Element; + +// @public (undocumented) +export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonProps): JSX.Element | null; + +// @public (undocumented) +export interface ToggleToolLockedButtonProps { + // (undocumented) + activeToolId?: string; +} + +// @public (undocumented) +export function ToggleToolLockItem(): JSX.Element; + +// @public (undocumented) +export function ToggleTransparentBgMenuItem(): JSX.Element; + +// @public (undocumented) +export function ToggleWrapModeItem(): JSX.Element; + +// @public (undocumented) +export function ToolbarItem({ tool }: ToolbarItemProps): JSX.Element; + +// @public (undocumented) +export interface ToolbarItemProps { + // (undocumented) + tool: string; +} + +// @public (undocumented) +export function TrapezoidToolbarItem(): JSX.Element; + +// @public (undocumented) +export function TriangleToolbarItem(): JSX.Element; + +// @public (undocumented) +export function truncateStringWithEllipsis(str: string, maxLength: number): string; + +// @public (undocumented) +export function UndoRedoGroup(): JSX.Element; + +// @public (undocumented) +export function UngroupMenuItem(): JSX.Element | null; + +// @public +export const unknownEmbedShapePermissionOverrides: TLEmbedShapePermissions; + +// @public (undocumented) +export function UnlockAllMenuItem(): JSX.Element; + +// @public (undocumented) +export function unwrapLabel(label?: TLUiActionItem['label'], menuType?: string): string | undefined; + +// @public +export function updateArrowTargetState({ editor, pointInPageSpace, arrow, isPrecise, currentBinding, oppositeBinding, }: UpdateArrowTargetStateOpts): ArrowTargetState | null; + +// @public +export interface UpdateArrowTargetStateOpts { + // (undocumented) + arrow: TLArrowShape | undefined; + // (undocumented) + currentBinding: TLArrowBinding | undefined; + // (undocumented) + editor: Editor; + // (undocumented) + isPrecise: boolean; + oppositeBinding: TLArrowBinding | undefined; + // (undocumented) + pointInPageSpace: VecLike; +} + +// @public (undocumented) +export function useA11y(): TLUiA11yContextType; + +// @public (undocumented) +export function useActions(): TLUiActionsContextType; + +// @public (undocumented) +export function useAssetUrls(): TLUiAssetUrls; + +// @public (undocumented) +export function useBreakpoint(): number; + +// @public +export function useCanApplySelectionAction(): boolean; + +// @public (undocumented) +export function useCanRedo(): boolean; + +// @public (undocumented) +export function useCanUndo(): boolean; + +// @public (undocumented) +export function useCollaborationStatus(): "offline" | "online" | null; + +// @public (undocumented) +export function useCopyAs(): (ids: TLShapeId[], format?: TLCopyType) => void; + +// @public (undocumented) +export function useCurrentTranslation(): TLUiTranslation; + +// @public (undocumented) +export function useDefaultHelpers(): { + addDialog: (dialog: Omit & { + id?: string; + }) => string; + addToast: (toast: Omit & { + id?: string; + }) => string; + clearDialogs: () => void; + clearToasts: () => void; + copy: (source: TLUiEventSource) => Promise; + copyAs: (ids: TLShapeId_2[], format?: TLCopyType) => void; + cut: (source: TLUiEventSource) => Promise; + exportAs: (ids: TLShapeId_2[], opts?: { + format?: TLExportType; + name?: string; + scale?: number; + }) => void; + getEmbedDefinition: (url: string) => TLEmbedResult; + insertMedia: () => Promise; + isMobile: boolean; + msg: (id?: Exclude | string) => string; + paste: (data: ClipboardItem[] | DataTransfer, source: TLUiEventSource, point?: VecLike) => Promise; + printSelectionOrPages: () => Promise; + removeDialog: (id: string) => string; + removeToast: (id: TLUiToast['id']) => string; + replaceImage: () => Promise; + replaceVideo: () => Promise; +}; + +// @public (undocumented) +export function useDialogs(): TLUiDialogsContextType; + +// @public +export function useDirection(): "ltr" | "rtl"; + +// @public (undocumented) +export function useEditablePlainText(shapeId: TLShapeId, type: ExtractShapeByProps<{ + text: string; +}>['type'], text?: string): { + handleBlur: () => void; + handleChange: ({ plaintext }: { + plaintext: string; + }) => void; + handleDoubleClick: (e: { + nativeEvent: Event; + } | Event) => void; + handleFocus: () => void; + handleInputPointerDown: (e: React_3.PointerEvent) => void; + handleKeyDown: (e: KeyboardEvent) => void; + handlePaste: (e: ClipboardEvent | React_3.ClipboardEvent) => void; + isEditing: boolean; + isEmpty: boolean; + isReadyForEditing: boolean; + rInput: React_3.RefObject; +}; + +// @public (undocumented) +export function useEditableRichText(shapeId: TLShapeId, type: ExtractShapeByProps<{ + richText: TLRichText_2; +}>['type'], richText?: TLRichText_2): { + handleBlur: () => void; + handleChange: ({ richText }: { + richText: TLRichText_2; + }) => void; + handleDoubleClick: (e: { + nativeEvent: Event; + } | Event) => void; + handleFocus: () => void; + handleInputPointerDown: (e: React.PointerEvent) => void; + handleKeyDown: (e: KeyboardEvent) => void; + handlePaste: (e: ClipboardEvent | React.ClipboardEvent) => void; + isEditing: boolean; + isEmpty: boolean | undefined; + isReadyForEditing: boolean; + rInput: RefObject; +}; + +// @public (undocumented) +export function useExportAs(): (ids: TLShapeId[], opts?: { + format?: TLExportType; + name?: string; + scale?: number; +}) => void; + +// @public +export function useImageOrVideoAsset({ shapeId, assetId, width }: UseImageOrVideoAssetOptions): { + asset: (TLImageAsset | TLVideoAsset) | null; + url: null | string; +}; + +// @public +export interface UseImageOrVideoAssetOptions { + assetId: null | TLAssetId; + shapeId?: TLShapeId; + width: number; +} + +// @public (undocumented) +export function useIsToolSelected(tool: TLUiToolItem | undefined): boolean; + +// @public (undocumented) +export function useKeyboardShortcuts(): void; + +// @public (undocumented) +export function useLocalStorageState(key: string, defaultValue: T): readonly [T, (setter: ((value: T) => T) | T) => void]; + +// @public +export function useMaybeCurrentTranslation(): null | TLUiTranslation; + +// @public (undocumented) +export function useMenuClipboardEvents(): { + copy: (source: TLUiEventSource) => Promise; + cut: (source: TLUiEventSource) => Promise; + paste: (data: ClipboardItem[] | DataTransfer, source: TLUiEventSource, point?: VecLike) => Promise; +}; + +// @public (undocumented) +export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void): readonly [boolean, (isOpen: boolean) => void]; + +// @public (undocumented) +export function useNativeClipboardEvents(): void; + +// @public (undocumented) +export function usePrefersReducedMotion(): boolean; + +// @public (undocumented) +export function useReadonly(): boolean; + +// @public (undocumented) +export function useRelevantStyles(stylesToCheck?: readonly StyleProp[]): null | ReadonlySharedStyleMap; + +// @public (undocumented) +export function useSelectedShapesAnnouncer(): void; + +// @public (undocumented) +export function useShowCollaborationUi(): boolean; + +// @public (undocumented) +export function useStylePanelContext(): StylePanelContext; + +// @public (undocumented) +export function useTldrawUiComponents(): TLUiComponents; + +// @public (undocumented) +export function useTldrawUiOrientation(): TldrawUiOrientationContext; + +// @public (undocumented) +export function useToasts(): TLUiToastsContextType; + +// @public (undocumented) +export function useTools(): TLUiToolsContextType; + +// @public +export function useTranslation(): (id?: Exclude | string) => string; + +// @public (undocumented) +export function useUiEvents(): TLUiEventContextType; + +// @public +export function useUnlockedSelectedShapesCount(min?: number, max?: number): boolean | number; + +// @public (undocumented) +export class VideoAssetUtil extends AssetUtil { + // (undocumented) + getAssetFromFile(file: File, assetId: TLAssetId): Promise; + // (undocumented) + getDefaultProps(): TLVideoAsset['props']; + // (undocumented) + getSupportedMimeTypes(): readonly string[]; + // (undocumented) + static migrations: MigrationSequence; + // (undocumented) + options: { + supportedMimeTypes: null | readonly string[]; + }; + // (undocumented) + static props: { + fileSize: Validator; + h: Validator; + isAnimated: Validator; + mimeType: Validator; + name: Validator; + src: Validator; + w: Validator; + }; + // (undocumented) + static type: 'video'; +} + +// @public (undocumented) +export interface VideoShapeOptions extends ShapeOptionsWithDisplayValues { + autoplay: boolean; +} + +// @public (undocumented) +export class VideoShapeUtil extends BaseBoxShapeUtil { + // (undocumented) + canEdit(shape: TLVideoShape): boolean; + // (undocumented) + component(shape: TLVideoShape): JSX.Element; + // (undocumented) + createShapeForAsset(asset: TLAsset, position: VecModel): null | TLShapePartial; + // (undocumented) + getAriaDescriptor(shape: TLVideoShape): string; + // (undocumented) + getDefaultProps(): TLVideoShape['props']; + // (undocumented) + getIndicatorPath(shape: TLVideoShape): Path2D; + // (undocumented) + static handledAssetTypes: readonly ['video']; + // (undocumented) + isAspectRatioLocked(shape: TLVideoShape): boolean; + // (undocumented) + static migrations: TLPropsMigrations; + // (undocumented) + options: VideoShapeOptions; + // (undocumented) + static props: RecordProps; + // (undocumented) + toSvg(shape: TLVideoShape, ctx: SvgExportContext): Promise; + // (undocumented) + static type: 'video'; +} + +// @public (undocumented) +export interface VideoShapeUtilDisplayValues { +} + +// @public (undocumented) +export function ViewSubmenu(): JSX.Element; + +// @public +export const WrapSelectionExtension: Extension; + +// @public +export interface WrapSelectionOptions { + pairs: Record; +} + +// @public (undocumented) +export function XBoxToolbarItem(): JSX.Element; + +// @public +export class ZoomBrushOverlayUtil extends OverlayUtil { + // (undocumented) + getOverlays(): TLZoomBrushOverlay[]; + // (undocumented) + isActive(): boolean; + // (undocumented) + options: { + lineWidth: number; + zIndex: number; + }; + // (undocumented) + render(ctx: CanvasRenderingContext2D, overlays: TLZoomBrushOverlay[]): void; + // (undocumented) + renderMinimap(ctx: CanvasRenderingContext2D, overlays: TLZoomBrushOverlay[], zoom: number): void; + // (undocumented) + static type: string; +} + +// @public (undocumented) +export function ZoomOrRotateMenuItem(): JSX.Element; + +// @public (undocumented) +export function ZoomTo100MenuItem(): JSX.Element; + +// @public (undocumented) +export function ZoomToFitMenuItem(): JSX.Element; + +// @public (undocumented) +export class ZoomTool extends StateNode { + // (undocumented) + static children(): TLStateNodeConstructor[]; + // (undocumented) + static id: string; + // (undocumented) + info: TLPointerEventInfo & { + onInteractionEnd?: string; + }; + // (undocumented) + static initial: string; + // (undocumented) + static isLockable: boolean; + // (undocumented) + onEnter(info: TLPointerEventInfo & { + onInteractionEnd: string; + }): void; + // (undocumented) + onExit(): void; + // (undocumented) + onInterrupt(): void; + // (undocumented) + onKeyDown(): void; + // (undocumented) + onKeyUp(info: TLKeyboardEventInfo): void; +} + +// @public (undocumented) +export function ZoomToSelectionMenuItem(): JSX.Element; + + +export * from "@tldraw/editor"; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/templates/image-pipeline/worker-configuration.d.ts b/templates/image-pipeline/worker-configuration.d.ts new file mode 100644 index 0000000..60be877 --- /dev/null +++ b/templates/image-pipeline/worker-configuration.d.ts @@ -0,0 +1,8572 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types` (hash: be2c5b528d78ebdbb8b143ce5eadd706) +// Runtime types generated with workerd@1.20250913.0 2025-05-08 nodejs_compat +declare namespace Cloudflare { + interface Env { + IMAGE_BUCKET: R2Bucket + REPLICATE_API_TOKEN?: string + } +} +interface Env extends Cloudflare.Env {} + +// Begin runtime types +/*! ***************************************************************************** +Copyright (c) Cloudflare. All rights reserved. +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* eslint-disable */ +// noinspection JSUnusedGlobalSymbols +declare var onmessage: never +/** + * An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) + */ +declare class DOMException extends Error { + constructor(message?: string, name?: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */ + readonly message: string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */ + readonly name: string + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) + */ + readonly code: number + static readonly INDEX_SIZE_ERR: number + static readonly DOMSTRING_SIZE_ERR: number + static readonly HIERARCHY_REQUEST_ERR: number + static readonly WRONG_DOCUMENT_ERR: number + static readonly INVALID_CHARACTER_ERR: number + static readonly NO_DATA_ALLOWED_ERR: number + static readonly NO_MODIFICATION_ALLOWED_ERR: number + static readonly NOT_FOUND_ERR: number + static readonly NOT_SUPPORTED_ERR: number + static readonly INUSE_ATTRIBUTE_ERR: number + static readonly INVALID_STATE_ERR: number + static readonly SYNTAX_ERR: number + static readonly INVALID_MODIFICATION_ERR: number + static readonly NAMESPACE_ERR: number + static readonly INVALID_ACCESS_ERR: number + static readonly VALIDATION_ERR: number + static readonly TYPE_MISMATCH_ERR: number + static readonly SECURITY_ERR: number + static readonly NETWORK_ERR: number + static readonly ABORT_ERR: number + static readonly URL_MISMATCH_ERR: number + static readonly QUOTA_EXCEEDED_ERR: number + static readonly TIMEOUT_ERR: number + static readonly INVALID_NODE_TYPE_ERR: number + static readonly DATA_CLONE_ERR: number + get stack(): any + set stack(value: any) +} +type WorkerGlobalScopeEventMap = { + fetch: FetchEvent + scheduled: ScheduledEvent + queue: QueueEvent + unhandledrejection: PromiseRejectionEvent + rejectionhandled: PromiseRejectionEvent +} +declare abstract class WorkerGlobalScope extends EventTarget { + EventTarget: typeof EventTarget +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */ +interface Console { + 'assert'(condition?: boolean, ...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */ + clear(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */ + count(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */ + countReset(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */ + debug(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) */ + dir(item?: any, options?: any): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) */ + dirxml(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */ + error(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */ + group(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */ + groupCollapsed(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */ + groupEnd(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */ + info(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */ + log(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) */ + table(tabularData?: any, properties?: string[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */ + time(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */ + timeEnd(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */ + timeLog(label?: string, ...data: any[]): void + timeStamp(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */ + trace(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */ + warn(...data: any[]): void +} +declare const console: Console +type BufferSource = ArrayBufferView | ArrayBuffer +type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array +declare namespace WebAssembly { + class CompileError extends Error { + constructor(message?: string) + } + class RuntimeError extends Error { + constructor(message?: string) + } + type ValueType = 'anyfunc' | 'externref' | 'f32' | 'f64' | 'i32' | 'i64' | 'v128' + interface GlobalDescriptor { + value: ValueType + mutable?: boolean + } + class Global { + constructor(descriptor: GlobalDescriptor, value?: any) + value: any + valueOf(): any + } + type ImportValue = ExportValue | number + type ModuleImports = Record + type Imports = Record + type ExportValue = Function | Global | Memory | Table + type Exports = Record + class Instance { + constructor(module: Module, imports?: Imports) + readonly exports: Exports + } + interface MemoryDescriptor { + initial: number + maximum?: number + shared?: boolean + } + class Memory { + constructor(descriptor: MemoryDescriptor) + readonly buffer: ArrayBuffer + grow(delta: number): number + } + type ImportExportKind = 'function' | 'global' | 'memory' | 'table' + interface ModuleExportDescriptor { + kind: ImportExportKind + name: string + } + interface ModuleImportDescriptor { + kind: ImportExportKind + module: string + name: string + } + abstract class Module { + static customSections(module: Module, sectionName: string): ArrayBuffer[] + static exports(module: Module): ModuleExportDescriptor[] + static imports(module: Module): ModuleImportDescriptor[] + } + type TableKind = 'anyfunc' | 'externref' + interface TableDescriptor { + element: TableKind + initial: number + maximum?: number + } + class Table { + constructor(descriptor: TableDescriptor, value?: any) + readonly length: number + get(index: number): any + grow(delta: number, value?: any): number + set(index: number, value?: any): void + } + function instantiate(module: Module, imports?: Imports): Promise + function validate(bytes: BufferSource): boolean +} +/** + * This ServiceWorker API interface represents the global execution context of a service worker. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope) + */ +interface ServiceWorkerGlobalScope extends WorkerGlobalScope { + DOMException: typeof DOMException + WorkerGlobalScope: typeof WorkerGlobalScope + btoa(data: string): string + atob(data: string): string + setTimeout(callback: (...args: any[]) => void, msDelay?: number): number + setTimeout( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args + ): number + clearTimeout(timeoutId: number | null): void + setInterval(callback: (...args: any[]) => void, msDelay?: number): number + setInterval( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args + ): number + clearInterval(timeoutId: number | null): void + queueMicrotask(task: Function): void + structuredClone(value: T, options?: StructuredSerializeOptions): T + reportError(error: any): void + fetch(input: RequestInfo | URL, init?: RequestInit): Promise + self: ServiceWorkerGlobalScope + crypto: Crypto + caches: CacheStorage + scheduler: Scheduler + performance: Performance + Cloudflare: Cloudflare + readonly origin: string + Event: typeof Event + ExtendableEvent: typeof ExtendableEvent + CustomEvent: typeof CustomEvent + PromiseRejectionEvent: typeof PromiseRejectionEvent + FetchEvent: typeof FetchEvent + TailEvent: typeof TailEvent + TraceEvent: typeof TailEvent + ScheduledEvent: typeof ScheduledEvent + MessageEvent: typeof MessageEvent + CloseEvent: typeof CloseEvent + ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader + ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader + ReadableStream: typeof ReadableStream + WritableStream: typeof WritableStream + WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter + TransformStream: typeof TransformStream + ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy + CountQueuingStrategy: typeof CountQueuingStrategy + ErrorEvent: typeof ErrorEvent + EventSource: typeof EventSource + ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest + ReadableStreamDefaultController: typeof ReadableStreamDefaultController + ReadableByteStreamController: typeof ReadableByteStreamController + WritableStreamDefaultController: typeof WritableStreamDefaultController + TransformStreamDefaultController: typeof TransformStreamDefaultController + CompressionStream: typeof CompressionStream + DecompressionStream: typeof DecompressionStream + TextEncoderStream: typeof TextEncoderStream + TextDecoderStream: typeof TextDecoderStream + Headers: typeof Headers + Body: typeof Body + Request: typeof Request + Response: typeof Response + WebSocket: typeof WebSocket + WebSocketPair: typeof WebSocketPair + WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair + AbortController: typeof AbortController + AbortSignal: typeof AbortSignal + TextDecoder: typeof TextDecoder + TextEncoder: typeof TextEncoder + navigator: Navigator + Navigator: typeof Navigator + URL: typeof URL + URLSearchParams: typeof URLSearchParams + URLPattern: typeof URLPattern + Blob: typeof Blob + File: typeof File + FormData: typeof FormData + Crypto: typeof Crypto + SubtleCrypto: typeof SubtleCrypto + CryptoKey: typeof CryptoKey + CacheStorage: typeof CacheStorage + Cache: typeof Cache + FixedLengthStream: typeof FixedLengthStream + IdentityTransformStream: typeof IdentityTransformStream + HTMLRewriter: typeof HTMLRewriter + Performance: typeof Performance +} +declare function addEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetAddEventListenerOptions | boolean +): void +declare function removeEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetEventListenerOptions | boolean +): void +/** + * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ +declare function dispatchEvent( + event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap] +): boolean +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args +): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */ +declare function clearTimeout(timeoutId: number | null): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args +): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */ +declare function clearInterval(timeoutId: number | null): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */ +declare function queueMicrotask(task: Function): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */ +declare function structuredClone(value: T, options?: StructuredSerializeOptions): T +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ +declare function reportError(error: any): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ +declare function fetch( + input: RequestInfo | URL, + init?: RequestInit +): Promise +declare const self: ServiceWorkerGlobalScope +/** + * The Web Crypto API provides a set of low-level functions for common cryptographic tasks. + * The Workers runtime implements the full surface of this API, but with some differences in + * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) + * compared to those implemented in most browsers. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) + */ +declare const crypto: Crypto +/** + * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) + */ +declare const caches: CacheStorage +declare const scheduler: Scheduler +/** + * The Workers runtime supports a subset of the Performance API, used to measure timing and performance, + * as well as timing of subrequests and other operations. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) + */ +declare const performance: Performance +declare const Cloudflare: Cloudflare +declare const origin: string +declare const navigator: Navigator +interface TestController {} +interface ExecutionContext { + waitUntil(promise: Promise): void + passThroughOnException(): void + readonly props: Props +} +type ExportedHandlerFetchHandler = ( + request: Request>, + env: Env, + ctx: ExecutionContext +) => Response | Promise +type ExportedHandlerTailHandler = ( + events: TraceItem[], + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerTraceHandler = ( + traces: TraceItem[], + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerTailStreamHandler = ( + event: TailStream.TailEvent, + env: Env, + ctx: ExecutionContext +) => TailStream.TailEventHandlerType | Promise +type ExportedHandlerScheduledHandler = ( + controller: ScheduledController, + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerQueueHandler = ( + batch: MessageBatch, + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerTestHandler = ( + controller: TestController, + env: Env, + ctx: ExecutionContext +) => void | Promise +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler + tail?: ExportedHandlerTailHandler + trace?: ExportedHandlerTraceHandler + tailStream?: ExportedHandlerTailStreamHandler + scheduled?: ExportedHandlerScheduledHandler + test?: ExportedHandlerTestHandler + email?: EmailExportedHandler + queue?: ExportedHandlerQueueHandler +} +interface StructuredSerializeOptions { + transfer?: any[] +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent) */ +declare abstract class PromiseRejectionEvent extends Event { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise) */ + readonly promise: Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason) */ + readonly reason: any +} +declare abstract class Navigator { + sendBeacon( + url: string, + body?: + | ReadableStream + | string + | (ArrayBuffer | ArrayBufferView) + | Blob + | FormData + | URLSearchParams + | URLSearchParams + ): boolean + readonly userAgent: string + readonly hardwareConcurrency: number +} +interface AlarmInvocationInfo { + readonly isRetry: boolean + readonly retryCount: number +} +interface Cloudflare { + readonly compatibilityFlags: Record +} +interface DurableObject { + fetch(request: Request): Response | Promise + alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise + webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise + webSocketClose?( + ws: WebSocket, + code: number, + reason: string, + wasClean: boolean + ): void | Promise + webSocketError?(ws: WebSocket, error: unknown): void | Promise +} +type DurableObjectStub = Fetcher< + T, + 'alarm' | 'webSocketMessage' | 'webSocketClose' | 'webSocketError' +> & { + readonly id: DurableObjectId + readonly name?: string +} +interface DurableObjectId { + toString(): string + equals(other: DurableObjectId): boolean + readonly name?: string +} +declare abstract class DurableObjectNamespace< + T extends Rpc.DurableObjectBranded | undefined = undefined, +> { + newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId + idFromName(name: string): DurableObjectId + idFromString(id: string): DurableObjectId + get( + id: DurableObjectId, + options?: DurableObjectNamespaceGetDurableObjectOptions + ): DurableObjectStub + getByName( + name: string, + options?: DurableObjectNamespaceGetDurableObjectOptions + ): DurableObjectStub + jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace +} +type DurableObjectJurisdiction = 'eu' | 'fedramp' | 'fedramp-high' +interface DurableObjectNamespaceNewUniqueIdOptions { + jurisdiction?: DurableObjectJurisdiction +} +type DurableObjectLocationHint = + | 'wnam' + | 'enam' + | 'sam' + | 'weur' + | 'eeur' + | 'apac' + | 'oc' + | 'afr' + | 'me' +interface DurableObjectNamespaceGetDurableObjectOptions { + locationHint?: DurableObjectLocationHint +} +interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {} +interface DurableObjectState { + waitUntil(promise: Promise): void + readonly props: Props + readonly id: DurableObjectId + readonly storage: DurableObjectStorage + container?: Container + blockConcurrencyWhile(callback: () => Promise): Promise + acceptWebSocket(ws: WebSocket, tags?: string[]): void + getWebSockets(tag?: string): WebSocket[] + setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void + getWebSocketAutoResponse(): WebSocketRequestResponsePair | null + getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null + setHibernatableWebSocketEventTimeout(timeoutMs?: number): void + getHibernatableWebSocketEventTimeout(): number | null + getTags(ws: WebSocket): string[] + abort(reason?: string): void +} +interface DurableObjectTransaction { + get(key: string, options?: DurableObjectGetOptions): Promise + get(keys: string[], options?: DurableObjectGetOptions): Promise> + list(options?: DurableObjectListOptions): Promise> + put(key: string, value: T, options?: DurableObjectPutOptions): Promise + put(entries: Record, options?: DurableObjectPutOptions): Promise + delete(key: string, options?: DurableObjectPutOptions): Promise + delete(keys: string[], options?: DurableObjectPutOptions): Promise + rollback(): void + getAlarm(options?: DurableObjectGetAlarmOptions): Promise + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise +} +interface DurableObjectStorage { + get(key: string, options?: DurableObjectGetOptions): Promise + get(keys: string[], options?: DurableObjectGetOptions): Promise> + list(options?: DurableObjectListOptions): Promise> + put(key: string, value: T, options?: DurableObjectPutOptions): Promise + put(entries: Record, options?: DurableObjectPutOptions): Promise + delete(key: string, options?: DurableObjectPutOptions): Promise + delete(keys: string[], options?: DurableObjectPutOptions): Promise + deleteAll(options?: DurableObjectPutOptions): Promise + transaction(closure: (txn: DurableObjectTransaction) => Promise): Promise + getAlarm(options?: DurableObjectGetAlarmOptions): Promise + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise + sync(): Promise + sql: SqlStorage + kv: SyncKvStorage + transactionSync(closure: () => T): T + getCurrentBookmark(): Promise + getBookmarkForTime(timestamp: number | Date): Promise + onNextSessionRestoreBookmark(bookmark: string): Promise +} +interface DurableObjectListOptions { + start?: string + startAfter?: string + end?: string + prefix?: string + reverse?: boolean + limit?: number + allowConcurrency?: boolean + noCache?: boolean +} +interface DurableObjectGetOptions { + allowConcurrency?: boolean + noCache?: boolean +} +interface DurableObjectGetAlarmOptions { + allowConcurrency?: boolean +} +interface DurableObjectPutOptions { + allowConcurrency?: boolean + allowUnconfirmed?: boolean + noCache?: boolean +} +interface DurableObjectSetAlarmOptions { + allowConcurrency?: boolean + allowUnconfirmed?: boolean +} +declare class WebSocketRequestResponsePair { + constructor(request: string, response: string) + get request(): string + get response(): string +} +interface AnalyticsEngineDataset { + writeDataPoint(event?: AnalyticsEngineDataPoint): void +} +interface AnalyticsEngineDataPoint { + indexes?: ((ArrayBuffer | string) | null)[] + doubles?: number[] + blobs?: ((ArrayBuffer | string) | null)[] +} +/** + * An event which takes place in the DOM. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event) + */ +declare class Event { + constructor(type: string, init?: EventInit) + /** + * Returns the type of event, e.g. "click", "hashchange", or "submit". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type) + */ + get type(): string + /** + * Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase) + */ + get eventPhase(): number + /** + * Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed) + */ + get composed(): boolean + /** + * Returns true or false depending on how event was initialized. True if event goes through its target's ancestors in reverse tree order, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles) + */ + get bubbles(): boolean + /** + * Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable) + */ + get cancelable(): boolean + /** + * Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented) + */ + get defaultPrevented(): boolean + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue) + */ + get returnValue(): boolean + /** + * Returns the object whose event listener's callback is currently being invoked. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget) + */ + get currentTarget(): EventTarget | undefined + /** + * Returns the object to which event is dispatched (its target). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target) + */ + get target(): EventTarget | undefined + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement) + */ + get srcElement(): EventTarget | undefined + /** + * Returns the event's timestamp as the number of milliseconds measured relative to the time origin. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp) + */ + get timeStamp(): number + /** + * Returns true if event was dispatched by the user agent, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted) + */ + get isTrusted(): boolean + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + get cancelBubble(): boolean + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + set cancelBubble(value: boolean) + /** + * Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation) + */ + stopImmediatePropagation(): void + /** + * If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault) + */ + preventDefault(): void + /** + * When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation) + */ + stopPropagation(): void + /** + * Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath) + */ + composedPath(): EventTarget[] + static readonly NONE: number + static readonly CAPTURING_PHASE: number + static readonly AT_TARGET: number + static readonly BUBBLING_PHASE: number +} +interface EventInit { + bubbles?: boolean + cancelable?: boolean + composed?: boolean +} +type EventListener = (event: EventType) => void +interface EventListenerObject { + handleEvent(event: EventType): void +} +type EventListenerOrEventListenerObject = + | EventListener + | EventListenerObject +/** + * EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget) + */ +declare class EventTarget = Record> { + constructor() + /** + * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. + * + * The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. + * + * When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. + * + * When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners. + * + * When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. + * + * If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted. + * + * The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener) + */ + addEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetAddEventListenerOptions | boolean + ): void + /** + * Removes the event listener in target's event listener list with the same type, callback, and options. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener) + */ + removeEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetEventListenerOptions | boolean + ): void + /** + * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ + dispatchEvent(event: EventMap[keyof EventMap]): boolean +} +interface EventTargetEventListenerOptions { + capture?: boolean +} +interface EventTargetAddEventListenerOptions { + capture?: boolean + passive?: boolean + once?: boolean + signal?: AbortSignal +} +interface EventTargetHandlerObject { + handleEvent: (event: Event) => any | undefined +} +/** + * A controller object that allows you to abort one or more DOM requests as and when desired. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController) + */ +declare class AbortController { + constructor() + /** + * Returns the AbortSignal object associated with this object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal) + */ + get signal(): AbortSignal + /** + * Invoking this method will set this object's AbortSignal's aborted flag and signal to any observers that the associated activity is to be aborted. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort) + */ + abort(reason?: any): void +} +/** + * A signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal) + */ +declare abstract class AbortSignal extends EventTarget { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static) */ + static abort(reason?: any): AbortSignal + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static) */ + static timeout(delay: number): AbortSignal + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static) */ + static any(signals: AbortSignal[]): AbortSignal + /** + * Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted) + */ + get aborted(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason) */ + get reason(): any + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + get onabort(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + set onabort(value: any | null) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) */ + throwIfAborted(): void +} +interface Scheduler { + wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise +} +interface SchedulerWaitOptions { + signal?: AbortSignal +} +/** + * Extends the lifetime of the install and activate events dispatched on the global scope as part of the service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent) + */ +declare abstract class ExtendableEvent extends Event { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) */ + waitUntil(promise: Promise): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) */ +declare class CustomEvent extends Event { + constructor(type: string, init?: CustomEventCustomEventInit) + /** + * Returns any custom data event was created with. Typically used for synthetic events. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail) + */ + get detail(): T +} +interface CustomEventCustomEventInit { + bubbles?: boolean + cancelable?: boolean + composed?: boolean + detail?: any +} +/** + * A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob) + */ +declare class Blob { + constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */ + get size(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */ + get type(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */ + slice(start?: number, end?: number, type?: string): Blob + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer) */ + arrayBuffer(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes) */ + bytes(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */ + text(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream) */ + stream(): ReadableStream +} +interface BlobOptions { + type?: string +} +/** + * Provides information about files and allows JavaScript in a web page to access their content. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File) + */ +declare class File extends Blob { + constructor( + bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, + name: string, + options?: FileOptions + ) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */ + get name(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */ + get lastModified(): number +} +interface FileOptions { + type?: string + lastModified?: number +} +/** + * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) + */ +declare abstract class CacheStorage { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open) */ + open(cacheName: string): Promise + readonly default: Cache +} +/** + * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) + */ +declare abstract class Cache { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */ + delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */ + match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */ + put(request: RequestInfo | URL, response: Response): Promise +} +interface CacheQueryOptions { + ignoreMethod?: boolean +} +/** + * The Web Crypto API provides a set of low-level functions for common cryptographic tasks. + * The Workers runtime implements the full surface of this API, but with some differences in + * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) + * compared to those implemented in most browsers. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) + */ +declare abstract class Crypto { + /** + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) + */ + get subtle(): SubtleCrypto + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) */ + getRandomValues< + T extends + | Int8Array + | Uint8Array + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >(buffer: T): T + /** + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID) + */ + randomUUID(): string + DigestStream: typeof DigestStream +} +/** + * This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto). + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) + */ +declare abstract class SubtleCrypto { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) */ + encrypt( + algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + plainText: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) */ + decrypt( + algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + cipherText: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) */ + sign( + algorithm: string | SubtleCryptoSignAlgorithm, + key: CryptoKey, + data: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) */ + verify( + algorithm: string | SubtleCryptoSignAlgorithm, + key: CryptoKey, + signature: ArrayBuffer | ArrayBufferView, + data: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ + digest( + algorithm: string | SubtleCryptoHashAlgorithm, + data: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) */ + generateKey( + algorithm: string | SubtleCryptoGenerateKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) */ + deriveKey( + algorithm: string | SubtleCryptoDeriveKeyAlgorithm, + baseKey: CryptoKey, + derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) */ + deriveBits( + algorithm: string | SubtleCryptoDeriveKeyAlgorithm, + baseKey: CryptoKey, + length?: number | null + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) */ + importKey( + format: string, + keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, + algorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) */ + exportKey(format: string, key: CryptoKey): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) */ + wrapKey( + format: string, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) */ + unwrapKey( + format: string, + wrappedKey: ArrayBuffer | ArrayBufferView, + unwrappingKey: CryptoKey, + unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, + unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean +} +/** + * The CryptoKey dictionary of the Web Crypto API represents a cryptographic key. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey) + */ +declare abstract class CryptoKey { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) */ + readonly type: string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) */ + readonly extractable: boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) */ + readonly algorithm: + | CryptoKeyKeyAlgorithm + | CryptoKeyAesKeyAlgorithm + | CryptoKeyHmacKeyAlgorithm + | CryptoKeyRsaKeyAlgorithm + | CryptoKeyEllipticKeyAlgorithm + | CryptoKeyArbitraryKeyAlgorithm + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) */ + readonly usages: string[] +} +interface CryptoKeyPair { + publicKey: CryptoKey + privateKey: CryptoKey +} +interface JsonWebKey { + kty: string + use?: string + key_ops?: string[] + alg?: string + ext?: boolean + crv?: string + x?: string + y?: string + d?: string + n?: string + e?: string + p?: string + q?: string + dp?: string + dq?: string + qi?: string + oth?: RsaOtherPrimesInfo[] + k?: string +} +interface RsaOtherPrimesInfo { + r?: string + d?: string + t?: string +} +interface SubtleCryptoDeriveKeyAlgorithm { + name: string + salt?: ArrayBuffer | ArrayBufferView + iterations?: number + hash?: string | SubtleCryptoHashAlgorithm + $public?: CryptoKey + info?: ArrayBuffer | ArrayBufferView +} +interface SubtleCryptoEncryptAlgorithm { + name: string + iv?: ArrayBuffer | ArrayBufferView + additionalData?: ArrayBuffer | ArrayBufferView + tagLength?: number + counter?: ArrayBuffer | ArrayBufferView + length?: number + label?: ArrayBuffer | ArrayBufferView +} +interface SubtleCryptoGenerateKeyAlgorithm { + name: string + hash?: string | SubtleCryptoHashAlgorithm + modulusLength?: number + publicExponent?: ArrayBuffer | ArrayBufferView + length?: number + namedCurve?: string +} +interface SubtleCryptoHashAlgorithm { + name: string +} +interface SubtleCryptoImportKeyAlgorithm { + name: string + hash?: string | SubtleCryptoHashAlgorithm + length?: number + namedCurve?: string + compressed?: boolean +} +interface SubtleCryptoSignAlgorithm { + name: string + hash?: string | SubtleCryptoHashAlgorithm + dataLength?: number + saltLength?: number +} +interface CryptoKeyKeyAlgorithm { + name: string +} +interface CryptoKeyAesKeyAlgorithm { + name: string + length: number +} +interface CryptoKeyHmacKeyAlgorithm { + name: string + hash: CryptoKeyKeyAlgorithm + length: number +} +interface CryptoKeyRsaKeyAlgorithm { + name: string + modulusLength: number + publicExponent: ArrayBuffer | ArrayBufferView + hash?: CryptoKeyKeyAlgorithm +} +interface CryptoKeyEllipticKeyAlgorithm { + name: string + namedCurve: string +} +interface CryptoKeyArbitraryKeyAlgorithm { + name: string + hash?: CryptoKeyKeyAlgorithm + namedCurve?: string + length?: number +} +declare class DigestStream extends WritableStream { + constructor(algorithm: string | SubtleCryptoHashAlgorithm) + readonly digest: Promise + get bytesWritten(): number | bigint +} +/** + * A decoder for a specific method, that is a specific character encoding, like utf-8, iso-8859-2, koi8, cp1261, gbk, etc. A decoder takes a stream of bytes as input and emits a stream of code points. For a more scalable, non-native library, see StringView – a C-like representation of strings based on typed arrays. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) + */ +declare class TextDecoder { + constructor(label?: string, options?: TextDecoderConstructorOptions) + /** + * Returns the result of running encoding's decoder. The method can be invoked zero or more times with options's stream set to true, and then once without options's stream (or set to false), to process a fragmented input. If the invocation without options's stream (or set to false) has no input, it's clearest to omit both arguments. + * + * ``` + * var string = "", decoder = new TextDecoder(encoding), buffer; + * while(buffer = next_chunk()) { + * string += decoder.decode(buffer, {stream:true}); + * } + * string += decoder.decode(); // end-of-queue + * ``` + * + * If the error mode is "fatal" and encoding's decoder returns error, throws a TypeError. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) + */ + decode(input?: ArrayBuffer | ArrayBufferView, options?: TextDecoderDecodeOptions): string + get encoding(): string + get fatal(): boolean + get ignoreBOM(): boolean +} +/** + * TextEncoder takes a stream of code points as input and emits a stream of bytes. For a more scalable, non-native library, see StringView – a C-like representation of strings based on typed arrays. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) + */ +declare class TextEncoder { + constructor() + /** + * Returns the result of running UTF-8's encoder. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) + */ + encode(input?: string): Uint8Array + /** + * Runs the UTF-8 encoder on source, stores the result of that operation into destination, and returns the progress made as an object wherein read is the number of converted code units of source and written is the number of bytes modified in destination. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) + */ + encodeInto(input: string, buffer: ArrayBuffer | ArrayBufferView): TextEncoderEncodeIntoResult + get encoding(): string +} +interface TextDecoderConstructorOptions { + fatal: boolean + ignoreBOM: boolean +} +interface TextDecoderDecodeOptions { + stream: boolean +} +interface TextEncoderEncodeIntoResult { + read: number + written: number +} +/** + * Events providing information related to errors in scripts or in files. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent) + */ +declare class ErrorEvent extends Event { + constructor(type: string, init?: ErrorEventErrorEventInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename) */ + get filename(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message) */ + get message(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno) */ + get lineno(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno) */ + get colno(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error) */ + get error(): any +} +interface ErrorEventErrorEventInit { + message?: string + filename?: string + lineno?: number + colno?: number + error?: any +} +/** + * A message received by a target object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) + */ +declare class MessageEvent extends Event { + constructor(type: string, initializer: MessageEventInit) + /** + * Returns the data of the message. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) + */ + readonly data: any + /** + * Returns the origin of the message, for server-sent events and cross-document messaging. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) + */ + readonly origin: string | null + /** + * Returns the last event ID string, for server-sent events. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId) + */ + readonly lastEventId: string + /** + * Returns the WindowProxy of the source window, for cross-document messaging, and the MessagePort being attached, in the connect event fired at SharedWorkerGlobalScope objects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source) + */ + readonly source: MessagePort | null + /** + * Returns the MessagePort array sent with the message, for cross-document messaging and channel messaging. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports) + */ + readonly ports: MessagePort[] +} +interface MessageEventInit { + data: ArrayBuffer | string +} +/** + * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData) + */ +declare class FormData { + constructor() + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) */ + append(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) */ + append(name: string, value: Blob, filename?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete) */ + delete(name: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get) */ + get(name: string): (File | string) | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll) */ + getAll(name: string): (File | string)[] + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) */ + has(name: string): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) */ + set(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) */ + set(name: string, value: Blob, filename?: string): void + /* Returns an array of key, value pairs for every entry in the list. */ + entries(): IterableIterator<[key: string, value: File | string]> + /* Returns a list of keys in the list. */ + keys(): IterableIterator + /* Returns a list of values in the list. */ + values(): IterableIterator + forEach( + callback: (this: This, value: File | string, key: string, parent: FormData) => void, + thisArg?: This + ): void + [Symbol.iterator](): IterableIterator<[key: string, value: File | string]> +} +interface ContentOptions { + html?: boolean +} +declare class HTMLRewriter { + constructor() + on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter + onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter + transform(response: Response): Response +} +interface HTMLRewriterElementContentHandlers { + element?(element: Element): void | Promise + comments?(comment: Comment): void | Promise + text?(element: Text): void | Promise +} +interface HTMLRewriterDocumentContentHandlers { + doctype?(doctype: Doctype): void | Promise + comments?(comment: Comment): void | Promise + text?(text: Text): void | Promise + end?(end: DocumentEnd): void | Promise +} +interface Doctype { + readonly name: string | null + readonly publicId: string | null + readonly systemId: string | null +} +interface Element { + tagName: string + readonly attributes: IterableIterator + readonly removed: boolean + readonly namespaceURI: string + getAttribute(name: string): string | null + hasAttribute(name: string): boolean + setAttribute(name: string, value: string): Element + removeAttribute(name: string): Element + before(content: string | ReadableStream | Response, options?: ContentOptions): Element + after(content: string | ReadableStream | Response, options?: ContentOptions): Element + prepend(content: string | ReadableStream | Response, options?: ContentOptions): Element + append(content: string | ReadableStream | Response, options?: ContentOptions): Element + replace(content: string | ReadableStream | Response, options?: ContentOptions): Element + remove(): Element + removeAndKeepContent(): Element + setInnerContent(content: string | ReadableStream | Response, options?: ContentOptions): Element + onEndTag(handler: (tag: EndTag) => void | Promise): void +} +interface EndTag { + name: string + before(content: string | ReadableStream | Response, options?: ContentOptions): EndTag + after(content: string | ReadableStream | Response, options?: ContentOptions): EndTag + remove(): EndTag +} +interface Comment { + text: string + readonly removed: boolean + before(content: string, options?: ContentOptions): Comment + after(content: string, options?: ContentOptions): Comment + replace(content: string, options?: ContentOptions): Comment + remove(): Comment +} +interface Text { + readonly text: string + readonly lastInTextNode: boolean + readonly removed: boolean + before(content: string | ReadableStream | Response, options?: ContentOptions): Text + after(content: string | ReadableStream | Response, options?: ContentOptions): Text + replace(content: string | ReadableStream | Response, options?: ContentOptions): Text + remove(): Text +} +interface DocumentEnd { + append(content: string, options?: ContentOptions): DocumentEnd +} +/** + * This is the event type for fetch events dispatched on the service worker global scope. It contains information about the fetch, including the request and how the receiver will treat the response. It provides the event.respondWith() method, which allows us to provide a response to this fetch. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent) + */ +declare abstract class FetchEvent extends ExtendableEvent { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request) */ + readonly request: Request + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/respondWith) */ + respondWith(promise: Response | Promise): void + passThroughOnException(): void +} +type HeadersInit = Headers | Iterable> | Record +/** + * This Fetch API interface allows you to perform various actions on HTTP request and response headers. These actions include retrieving, setting, adding to, and removing. A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs.  You can add to this using methods like append() (see Examples.) In all methods of this interface, header names are matched by case-insensitive byte sequence. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers) + */ +declare class Headers { + constructor(init?: HeadersInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */ + get(name: string): string | null + getAll(name: string): string[] + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */ + getSetCookie(): string[] + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */ + has(name: string): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */ + set(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */ + append(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */ + delete(name: string): void + forEach( + callback: (this: This, value: string, key: string, parent: Headers) => void, + thisArg?: This + ): void + /* Returns an iterator allowing to go through all key/value pairs contained in this object. */ + entries(): IterableIterator<[key: string, value: string]> + /* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. */ + keys(): IterableIterator + /* Returns an iterator allowing to go through all values of the key/value pairs contained in this object. */ + values(): IterableIterator + [Symbol.iterator](): IterableIterator<[key: string, value: string]> +} +type BodyInit = + | ReadableStream + | string + | ArrayBuffer + | ArrayBufferView + | Blob + | URLSearchParams + | FormData +declare abstract class Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ + get body(): ReadableStream | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + get bodyUsed(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */ + bytes(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */ + text(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */ + json(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */ + formData(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob(): Promise +} +/** + * This Fetch API interface represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +declare var Response: { + prototype: Response + new (body?: BodyInit | null, init?: ResponseInit): Response + error(): Response + redirect(url: string, status?: number): Response + json(any: any, maybeInit?: ResponseInit | Response): Response +} +/** + * This Fetch API interface represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +interface Response extends Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/clone) */ + clone(): Response + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status) */ + status: number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText) */ + statusText: string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) */ + headers: Headers + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok) */ + ok: boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/redirected) */ + redirected: boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/url) */ + url: string + webSocket: WebSocket | null + cf: any | undefined + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/type) */ + type: 'default' | 'error' +} +interface ResponseInit { + status?: number + statusText?: string + headers?: HeadersInit + cf?: any + webSocket?: WebSocket | null + encodeBody?: 'automatic' | 'manual' +} +type RequestInfo> = + | Request + | string +/** + * This Fetch API interface represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +declare var Request: { + prototype: Request + new >( + input: RequestInfo | URL, + init?: RequestInit + ): Request +} +/** + * This Fetch API interface represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +interface Request> extends Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/clone) */ + clone(): Request + /** + * Returns request's HTTP method, which is "GET" by default. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/method) + */ + method: string + /** + * Returns the URL of request as a string. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url) + */ + url: string + /** + * Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers) + */ + headers: Headers + /** + * Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/redirect) + */ + redirect: string + fetcher: Fetcher | null + /** + * Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) + */ + signal: AbortSignal + cf: Cf | undefined + /** + * Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity) + */ + integrity: string + /** + * Returns a boolean indicating whether or not request can outlive the global in which it was created. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive) + */ + keepalive: boolean + /** + * Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache) + */ + cache?: 'no-store' +} +interface RequestInit { + /* A string to set request's method. */ + method?: string + /* A Headers object, an object literal, or an array of two-item arrays to set request's headers. */ + headers?: HeadersInit + /* A BodyInit object or null to set request's body. */ + body?: BodyInit | null + /* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */ + redirect?: string + fetcher?: Fetcher | null + cf?: Cf + /* A string indicating how the request will interact with the browser's cache to set request's cache. */ + cache?: 'no-store' + /* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */ + integrity?: string + /* An AbortSignal to set request's signal. */ + signal?: AbortSignal | null + encodeResponseBody?: 'automatic' | 'manual' +} +type Service< + T extends + | (new (...args: any[]) => Rpc.WorkerEntrypointBranded) + | Rpc.WorkerEntrypointBranded + | ExportedHandler + | undefined = undefined, +> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded + ? Fetcher> + : T extends Rpc.WorkerEntrypointBranded + ? Fetcher + : T extends Exclude + ? never + : Fetcher +type Fetcher< + T extends Rpc.EntrypointBranded | undefined = undefined, + Reserved extends string = never, +> = (T extends Rpc.EntrypointBranded + ? Rpc.Provider + : unknown) & { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise + connect(address: SocketAddress | string, options?: SocketOptions): Socket +} +interface KVNamespaceListKey { + name: Key + expiration?: number + metadata?: Metadata +} +type KVNamespaceListResult = + | { + list_complete: false + keys: KVNamespaceListKey[] + cursor: string + cacheStatus: string | null + } + | { + list_complete: true + keys: KVNamespaceListKey[] + cacheStatus: string | null + } +interface KVNamespace { + get(key: Key, options?: Partial>): Promise + get(key: Key, type: 'text'): Promise + get(key: Key, type: 'json'): Promise + get(key: Key, type: 'arrayBuffer'): Promise + get(key: Key, type: 'stream'): Promise + get(key: Key, options?: KVNamespaceGetOptions<'text'>): Promise + get( + key: Key, + options?: KVNamespaceGetOptions<'json'> + ): Promise + get(key: Key, options?: KVNamespaceGetOptions<'arrayBuffer'>): Promise + get(key: Key, options?: KVNamespaceGetOptions<'stream'>): Promise + get(key: Array, type: 'text'): Promise> + get( + key: Array, + type: 'json' + ): Promise> + get( + key: Array, + options?: Partial> + ): Promise> + get(key: Array, options?: KVNamespaceGetOptions<'text'>): Promise> + get( + key: Array, + options?: KVNamespaceGetOptions<'json'> + ): Promise> + list( + options?: KVNamespaceListOptions + ): Promise> + put( + key: Key, + value: string | ArrayBuffer | ArrayBufferView | ReadableStream, + options?: KVNamespacePutOptions + ): Promise + getWithMetadata( + key: Key, + options?: Partial> + ): Promise> + getWithMetadata( + key: Key, + type: 'text' + ): Promise> + getWithMetadata( + key: Key, + type: 'json' + ): Promise> + getWithMetadata( + key: Key, + type: 'arrayBuffer' + ): Promise> + getWithMetadata( + key: Key, + type: 'stream' + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'text'> + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'json'> + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'arrayBuffer'> + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'stream'> + ): Promise> + getWithMetadata( + key: Array, + type: 'text' + ): Promise>> + getWithMetadata( + key: Array, + type: 'json' + ): Promise>> + getWithMetadata( + key: Array, + options?: Partial> + ): Promise>> + getWithMetadata( + key: Array, + options?: KVNamespaceGetOptions<'text'> + ): Promise>> + getWithMetadata( + key: Array, + options?: KVNamespaceGetOptions<'json'> + ): Promise>> + delete(key: Key): Promise +} +interface KVNamespaceListOptions { + limit?: number + prefix?: string | null + cursor?: string | null +} +interface KVNamespaceGetOptions { + type: Type + cacheTtl?: number +} +interface KVNamespacePutOptions { + expiration?: number + expirationTtl?: number + metadata?: any | null +} +interface KVNamespaceGetWithMetadataResult { + value: Value | null + metadata: Metadata | null + cacheStatus: string | null +} +type QueueContentType = 'text' | 'bytes' | 'json' | 'v8' +interface Queue { + send(message: Body, options?: QueueSendOptions): Promise + sendBatch( + messages: Iterable>, + options?: QueueSendBatchOptions + ): Promise +} +interface QueueSendOptions { + contentType?: QueueContentType + delaySeconds?: number +} +interface QueueSendBatchOptions { + delaySeconds?: number +} +interface MessageSendRequest { + body: Body + contentType?: QueueContentType + delaySeconds?: number +} +interface QueueRetryOptions { + delaySeconds?: number +} +interface Message { + readonly id: string + readonly timestamp: Date + readonly body: Body + readonly attempts: number + retry(options?: QueueRetryOptions): void + ack(): void +} +interface QueueEvent extends ExtendableEvent { + readonly messages: readonly Message[] + readonly queue: string + retryAll(options?: QueueRetryOptions): void + ackAll(): void +} +interface MessageBatch { + readonly messages: readonly Message[] + readonly queue: string + retryAll(options?: QueueRetryOptions): void + ackAll(): void +} +interface R2Error extends Error { + readonly name: string + readonly code: number + readonly message: string + readonly action: string + readonly stack: any +} +interface R2ListOptions { + limit?: number + prefix?: string + cursor?: string + delimiter?: string + startAfter?: string + include?: ('httpMetadata' | 'customMetadata')[] +} +declare abstract class R2Bucket { + head(key: string): Promise + get( + key: string, + options: R2GetOptions & { + onlyIf: R2Conditional | Headers + } + ): Promise + get(key: string, options?: R2GetOptions): Promise + put( + key: string, + value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, + options?: R2PutOptions & { + onlyIf: R2Conditional | Headers + } + ): Promise + put( + key: string, + value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, + options?: R2PutOptions + ): Promise + createMultipartUpload(key: string, options?: R2MultipartOptions): Promise + resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload + delete(keys: string | string[]): Promise + list(options?: R2ListOptions): Promise +} +interface R2MultipartUpload { + readonly key: string + readonly uploadId: string + uploadPart( + partNumber: number, + value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob, + options?: R2UploadPartOptions + ): Promise + abort(): Promise + complete(uploadedParts: R2UploadedPart[]): Promise +} +interface R2UploadedPart { + partNumber: number + etag: string +} +declare abstract class R2Object { + readonly key: string + readonly version: string + readonly size: number + readonly etag: string + readonly httpEtag: string + readonly checksums: R2Checksums + readonly uploaded: Date + readonly httpMetadata?: R2HTTPMetadata + readonly customMetadata?: Record + readonly range?: R2Range + readonly storageClass: string + readonly ssecKeyMd5?: string + writeHttpMetadata(headers: Headers): void +} +interface R2ObjectBody extends R2Object { + get body(): ReadableStream + get bodyUsed(): boolean + arrayBuffer(): Promise + bytes(): Promise + text(): Promise + json(): Promise + blob(): Promise +} +type R2Range = + | { + offset: number + length?: number + } + | { + offset?: number + length: number + } + | { + suffix: number + } +interface R2Conditional { + etagMatches?: string + etagDoesNotMatch?: string + uploadedBefore?: Date + uploadedAfter?: Date + secondsGranularity?: boolean +} +interface R2GetOptions { + onlyIf?: R2Conditional | Headers + range?: R2Range | Headers + ssecKey?: ArrayBuffer | string +} +interface R2PutOptions { + onlyIf?: R2Conditional | Headers + httpMetadata?: R2HTTPMetadata | Headers + customMetadata?: Record + md5?: (ArrayBuffer | ArrayBufferView) | string + sha1?: (ArrayBuffer | ArrayBufferView) | string + sha256?: (ArrayBuffer | ArrayBufferView) | string + sha384?: (ArrayBuffer | ArrayBufferView) | string + sha512?: (ArrayBuffer | ArrayBufferView) | string + storageClass?: string + ssecKey?: ArrayBuffer | string +} +interface R2MultipartOptions { + httpMetadata?: R2HTTPMetadata | Headers + customMetadata?: Record + storageClass?: string + ssecKey?: ArrayBuffer | string +} +interface R2Checksums { + readonly md5?: ArrayBuffer + readonly sha1?: ArrayBuffer + readonly sha256?: ArrayBuffer + readonly sha384?: ArrayBuffer + readonly sha512?: ArrayBuffer + toJSON(): R2StringChecksums +} +interface R2StringChecksums { + md5?: string + sha1?: string + sha256?: string + sha384?: string + sha512?: string +} +interface R2HTTPMetadata { + contentType?: string + contentLanguage?: string + contentDisposition?: string + contentEncoding?: string + cacheControl?: string + cacheExpiry?: Date +} +type R2Objects = { + objects: R2Object[] + delimitedPrefixes: string[] +} & ( + | { + truncated: true + cursor: string + } + | { + truncated: false + } +) +interface R2UploadPartOptions { + ssecKey?: ArrayBuffer | string +} +declare abstract class ScheduledEvent extends ExtendableEvent { + readonly scheduledTime: number + readonly cron: string + noRetry(): void +} +interface ScheduledController { + readonly scheduledTime: number + readonly cron: string + noRetry(): void +} +interface QueuingStrategy { + highWaterMark?: number | bigint + size?: (chunk: T) => number | bigint +} +interface UnderlyingSink { + type?: string + start?: (controller: WritableStreamDefaultController) => void | Promise + write?: (chunk: W, controller: WritableStreamDefaultController) => void | Promise + abort?: (reason: any) => void | Promise + close?: () => void | Promise +} +interface UnderlyingByteSource { + type: 'bytes' + autoAllocateChunkSize?: number + start?: (controller: ReadableByteStreamController) => void | Promise + pull?: (controller: ReadableByteStreamController) => void | Promise + cancel?: (reason: any) => void | Promise +} +interface UnderlyingSource { + type?: '' | undefined + start?: (controller: ReadableStreamDefaultController) => void | Promise + pull?: (controller: ReadableStreamDefaultController) => void | Promise + cancel?: (reason: any) => void | Promise + expectedLength?: number | bigint +} +interface Transformer { + readableType?: string + writableType?: string + start?: (controller: TransformStreamDefaultController) => void | Promise + transform?: (chunk: I, controller: TransformStreamDefaultController) => void | Promise + flush?: (controller: TransformStreamDefaultController) => void | Promise + cancel?: (reason: any) => void | Promise + expectedLength?: number +} +interface StreamPipeOptions { + /** + * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + * + * Errors and closures of the source and destination streams propagate as follows: + * + * An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination. + * + * An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source. + * + * When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error. + * + * If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source. + * + * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. + */ + preventClose?: boolean + preventAbort?: boolean + preventCancel?: boolean + signal?: AbortSignal +} +type ReadableStreamReadResult = + | { + done: false + value: R + } + | { + done: true + value?: undefined + } +/** + * This Streams API interface represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +interface ReadableStream { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) */ + get locked(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel) */ + cancel(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) */ + getReader(): ReadableStreamDefaultReader + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) */ + getReader(options: ReadableStreamGetReaderOptions): ReadableStreamBYOBReader + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough) */ + pipeThrough( + transform: ReadableWritablePair, + options?: StreamPipeOptions + ): ReadableStream + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo) */ + pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee) */ + tee(): [ReadableStream, ReadableStream] + values(options?: ReadableStreamValuesOptions): AsyncIterableIterator + [Symbol.asyncIterator](options?: ReadableStreamValuesOptions): AsyncIterableIterator +} +/** + * This Streams API interface represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +declare const ReadableStream: { + prototype: ReadableStream + new ( + underlyingSource: UnderlyingByteSource, + strategy?: QueuingStrategy + ): ReadableStream + new ( + underlyingSource?: UnderlyingSource, + strategy?: QueuingStrategy + ): ReadableStream +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader) */ +declare class ReadableStreamDefaultReader { + constructor(stream: ReadableStream) + get closed(): Promise + cancel(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/read) */ + read(): Promise> + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/releaseLock) */ + releaseLock(): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader) */ +declare class ReadableStreamBYOBReader { + constructor(stream: ReadableStream) + get closed(): Promise + cancel(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) */ + read(view: T): Promise> + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) */ + releaseLock(): void + readAtLeast( + minElements: number, + view: T + ): Promise> +} +interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions { + min?: number +} +interface ReadableStreamGetReaderOptions { + /** + * Creates a ReadableStreamBYOBReader and locks the stream to the new reader. + * + * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation. + */ + mode: 'byob' +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest) */ +declare abstract class ReadableStreamBYOBRequest { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view) */ + get view(): Uint8Array | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond) */ + respond(bytesWritten: number): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView) */ + respondWithNewView(view: ArrayBuffer | ArrayBufferView): void + get atLeast(): number | null +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController) */ +declare abstract class ReadableStreamDefaultController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/close) */ + close(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue) */ + enqueue(chunk?: R): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/error) */ + error(reason: any): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController) */ +declare abstract class ReadableByteStreamController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/byobRequest) */ + get byobRequest(): ReadableStreamBYOBRequest | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/close) */ + close(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/enqueue) */ + enqueue(chunk: ArrayBuffer | ArrayBufferView): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/error) */ + error(reason: any): void +} +/** + * This Streams API interface represents a controller allowing control of a WritableStream's state. When constructing a WritableStream, the underlying sink is given a corresponding WritableStreamDefaultController instance to manipulate. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController) + */ +declare abstract class WritableStreamDefaultController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal) */ + get signal(): AbortSignal + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error) */ + error(reason?: any): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController) */ +declare abstract class TransformStreamDefaultController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue) */ + enqueue(chunk?: O): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/error) */ + error(reason: any): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate) */ + terminate(): void +} +interface ReadableWritablePair { + /** + * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + */ + writable: WritableStream + readable: ReadableStream +} +/** + * This Streams API interface provides a standard abstraction for writing streaming data to a destination, known as a sink. This object comes with built-in backpressure and queuing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream) + */ +declare class WritableStream { + constructor(underlyingSink?: UnderlyingSink, queuingStrategy?: QueuingStrategy) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/locked) */ + get locked(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/abort) */ + abort(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/close) */ + close(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/getWriter) */ + getWriter(): WritableStreamDefaultWriter +} +/** + * This Streams API interface is the object returned by WritableStream.getWriter() and once created locks the < writer to the WritableStream ensuring that no other streams can write to the underlying sink. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter) + */ +declare class WritableStreamDefaultWriter { + constructor(stream: WritableStream) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed) */ + get closed(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready) */ + get ready(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort) */ + abort(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close) */ + close(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write) */ + write(chunk?: W): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock) */ + releaseLock(): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream) */ +declare class TransformStream { + constructor( + transformer?: Transformer, + writableStrategy?: QueuingStrategy, + readableStrategy?: QueuingStrategy + ) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/readable) */ + get readable(): ReadableStream + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/writable) */ + get writable(): WritableStream +} +declare class FixedLengthStream extends IdentityTransformStream { + constructor( + expectedLength: number | bigint, + queuingStrategy?: IdentityTransformStreamQueuingStrategy + ) +} +declare class IdentityTransformStream extends TransformStream< + ArrayBuffer | ArrayBufferView, + Uint8Array +> { + constructor(queuingStrategy?: IdentityTransformStreamQueuingStrategy) +} +interface IdentityTransformStreamQueuingStrategy { + highWaterMark?: number | bigint +} +interface ReadableStreamValuesOptions { + preventCancel?: boolean +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CompressionStream) */ +declare class CompressionStream extends TransformStream { + constructor(format: 'gzip' | 'deflate' | 'deflate-raw') +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DecompressionStream) */ +declare class DecompressionStream extends TransformStream< + ArrayBuffer | ArrayBufferView, + Uint8Array +> { + constructor(format: 'gzip' | 'deflate' | 'deflate-raw') +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoderStream) */ +declare class TextEncoderStream extends TransformStream { + constructor() + get encoding(): string +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoderStream) */ +declare class TextDecoderStream extends TransformStream { + constructor(label?: string, options?: TextDecoderStreamTextDecoderStreamInit) + get encoding(): string + get fatal(): boolean + get ignoreBOM(): boolean +} +interface TextDecoderStreamTextDecoderStreamInit { + fatal?: boolean + ignoreBOM?: boolean +} +/** + * This Streams API interface provides a built-in byte length queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy) + */ +declare class ByteLengthQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/highWaterMark) */ + get highWaterMark(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/size) */ + get size(): (chunk?: any) => number +} +/** + * This Streams API interface provides a built-in byte length queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy) + */ +declare class CountQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/highWaterMark) */ + get highWaterMark(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/size) */ + get size(): (chunk?: any) => number +} +interface QueuingStrategyInit { + /** + * Creates a new ByteLengthQueuingStrategy with the provided high water mark. + * + * Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw. + */ + highWaterMark: number +} +interface ScriptVersion { + id?: string + tag?: string + message?: string +} +declare abstract class TailEvent extends ExtendableEvent { + readonly events: TraceItem[] + readonly traces: TraceItem[] +} +interface TraceItem { + readonly event: + | ( + | TraceItemFetchEventInfo + | TraceItemJsRpcEventInfo + | TraceItemScheduledEventInfo + | TraceItemAlarmEventInfo + | TraceItemQueueEventInfo + | TraceItemEmailEventInfo + | TraceItemTailEventInfo + | TraceItemCustomEventInfo + | TraceItemHibernatableWebSocketEventInfo + ) + | null + readonly eventTimestamp: number | null + readonly logs: TraceLog[] + readonly exceptions: TraceException[] + readonly diagnosticsChannelEvents: TraceDiagnosticChannelEvent[] + readonly scriptName: string | null + readonly entrypoint?: string + readonly scriptVersion?: ScriptVersion + readonly dispatchNamespace?: string + readonly scriptTags?: string[] + readonly durableObjectId?: string + readonly outcome: string + readonly executionModel: string + readonly truncated: boolean + readonly cpuTime: number + readonly wallTime: number +} +interface TraceItemAlarmEventInfo { + readonly scheduledTime: Date +} +interface TraceItemCustomEventInfo {} +interface TraceItemScheduledEventInfo { + readonly scheduledTime: number + readonly cron: string +} +interface TraceItemQueueEventInfo { + readonly queue: string + readonly batchSize: number +} +interface TraceItemEmailEventInfo { + readonly mailFrom: string + readonly rcptTo: string + readonly rawSize: number +} +interface TraceItemTailEventInfo { + readonly consumedEvents: TraceItemTailEventInfoTailItem[] +} +interface TraceItemTailEventInfoTailItem { + readonly scriptName: string | null +} +interface TraceItemFetchEventInfo { + readonly response?: TraceItemFetchEventInfoResponse + readonly request: TraceItemFetchEventInfoRequest +} +interface TraceItemFetchEventInfoRequest { + readonly cf?: any + readonly headers: Record + readonly method: string + readonly url: string + getUnredacted(): TraceItemFetchEventInfoRequest +} +interface TraceItemFetchEventInfoResponse { + readonly status: number +} +interface TraceItemJsRpcEventInfo { + readonly rpcMethod: string +} +interface TraceItemHibernatableWebSocketEventInfo { + readonly getWebSocketEvent: + | TraceItemHibernatableWebSocketEventInfoMessage + | TraceItemHibernatableWebSocketEventInfoClose + | TraceItemHibernatableWebSocketEventInfoError +} +interface TraceItemHibernatableWebSocketEventInfoMessage { + readonly webSocketEventType: string +} +interface TraceItemHibernatableWebSocketEventInfoClose { + readonly webSocketEventType: string + readonly code: number + readonly wasClean: boolean +} +interface TraceItemHibernatableWebSocketEventInfoError { + readonly webSocketEventType: string +} +interface TraceLog { + readonly timestamp: number + readonly level: string + readonly message: any +} +interface TraceException { + readonly timestamp: number + readonly message: string + readonly name: string + readonly stack?: string +} +interface TraceDiagnosticChannelEvent { + readonly timestamp: number + readonly channel: string + readonly message: any +} +interface TraceMetrics { + readonly cpuTime: number + readonly wallTime: number +} +interface UnsafeTraceMetrics { + fromTrace(item: TraceItem): TraceMetrics +} +/** + * The URL interface represents an object providing static methods used for creating object URLs. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL) + */ +declare class URL { + constructor(url: string | URL, base?: string | URL) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin) */ + get origin(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) */ + get href(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) */ + set href(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) */ + get protocol(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) */ + set protocol(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) */ + get username(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) */ + set username(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) */ + get password(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) */ + set password(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) */ + get host(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) */ + set host(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) */ + get hostname(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) */ + set hostname(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) */ + get port(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) */ + set port(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) */ + get pathname(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) */ + set pathname(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) */ + get search(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) */ + set search(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) */ + get hash(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) */ + set hash(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams) */ + get searchParams(): URLSearchParams + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON) */ + toJSON(): string + /*function toString() { [native code] }*/ + toString(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) */ + static canParse(url: string, base?: string): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/parse_static) */ + static parse(url: string, base?: string): URL | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static) */ + static createObjectURL(object: File | Blob): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static) */ + static revokeObjectURL(object_url: string): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams) */ +declare class URLSearchParams { + constructor(init?: Iterable> | Record | string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) */ + get size(): number + /** + * Appends a specified key/value pair as a new search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append) + */ + append(name: string, value: string): void + /** + * Deletes the given search parameter, and its associated value, from the list of all search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete) + */ + delete(name: string, value?: string): void + /** + * Returns the first value associated to the given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) + */ + get(name: string): string | null + /** + * Returns all the values association with a given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) + */ + getAll(name: string): string[] + /** + * Returns a Boolean indicating if such a search parameter exists. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) + */ + has(name: string, value?: string): boolean + /** + * Sets the value associated to a given search parameter to the given value. If there were several values, delete the others. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) + */ + set(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) */ + sort(): void + /* Returns an array of key, value pairs for every entry in the search params. */ + entries(): IterableIterator<[key: string, value: string]> + /* Returns a list of keys in the search params. */ + keys(): IterableIterator + /* Returns a list of values in the search params. */ + values(): IterableIterator + forEach( + callback: (this: This, value: string, key: string, parent: URLSearchParams) => void, + thisArg?: This + ): void + /*function toString() { [native code] } Returns a string containing a query string suitable for use in a URL. Does not include the question mark. */ + toString(): string + [Symbol.iterator](): IterableIterator<[key: string, value: string]> +} +declare class URLPattern { + constructor( + input?: string | URLPatternInit, + baseURL?: string | URLPatternOptions, + patternOptions?: URLPatternOptions + ) + get protocol(): string + get username(): string + get password(): string + get hostname(): string + get port(): string + get pathname(): string + get search(): string + get hash(): string + get hasRegExpGroups(): boolean + test(input?: string | URLPatternInit, baseURL?: string): boolean + exec(input?: string | URLPatternInit, baseURL?: string): URLPatternResult | null +} +interface URLPatternInit { + protocol?: string + username?: string + password?: string + hostname?: string + port?: string + pathname?: string + search?: string + hash?: string + baseURL?: string +} +interface URLPatternComponentResult { + input: string + groups: Record +} +interface URLPatternResult { + inputs: (string | URLPatternInit)[] + protocol: URLPatternComponentResult + username: URLPatternComponentResult + password: URLPatternComponentResult + hostname: URLPatternComponentResult + port: URLPatternComponentResult + pathname: URLPatternComponentResult + search: URLPatternComponentResult + hash: URLPatternComponentResult +} +interface URLPatternOptions { + ignoreCase?: boolean +} +/** + * A CloseEvent is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent) + */ +declare class CloseEvent extends Event { + constructor(type: string, initializer?: CloseEventInit) + /** + * Returns the WebSocket connection close code provided by the server. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code) + */ + readonly code: number + /** + * Returns the WebSocket connection close reason provided by the server. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason) + */ + readonly reason: string + /** + * Returns true if the connection closed cleanly; false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean) + */ + readonly wasClean: boolean +} +interface CloseEventInit { + code?: number + reason?: string + wasClean?: boolean +} +type WebSocketEventMap = { + close: CloseEvent + message: MessageEvent + open: Event + error: ErrorEvent +} +/** + * Provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +declare var WebSocket: { + prototype: WebSocket + new (url: string, protocols?: string[] | string): WebSocket + readonly READY_STATE_CONNECTING: number + readonly CONNECTING: number + readonly READY_STATE_OPEN: number + readonly OPEN: number + readonly READY_STATE_CLOSING: number + readonly CLOSING: number + readonly READY_STATE_CLOSED: number + readonly CLOSED: number +} +/** + * Provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +interface WebSocket extends EventTarget { + accept(): void + /** + * Transmits data using the WebSocket connection. data can be a string, a Blob, an ArrayBuffer, or an ArrayBufferView. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/send) + */ + send(message: (ArrayBuffer | ArrayBufferView) | string): void + /** + * Closes the WebSocket connection, optionally using code as the the WebSocket connection close code and reason as the the WebSocket connection close reason. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/close) + */ + close(code?: number, reason?: string): void + serializeAttachment(attachment: any): void + deserializeAttachment(): any | null + /** + * Returns the state of the WebSocket object's connection. It can have the values described below. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/readyState) + */ + readyState: number + /** + * Returns the URL that was used to establish the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/url) + */ + url: string | null + /** + * Returns the subprotocol selected by the server, if any. It can be used in conjunction with the array form of the constructor's second argument to perform subprotocol negotiation. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/protocol) + */ + protocol: string | null + /** + * Returns the extensions selected by the server, if any. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) + */ + extensions: string | null +} +declare const WebSocketPair: { + new (): { + 0: WebSocket + 1: WebSocket + } +} +interface SqlStorage { + exec>( + query: string, + ...bindings: any[] + ): SqlStorageCursor + get databaseSize(): number + Cursor: typeof SqlStorageCursor + Statement: typeof SqlStorageStatement +} +declare abstract class SqlStorageStatement {} +type SqlStorageValue = ArrayBuffer | string | number | null +declare abstract class SqlStorageCursor> { + next(): + | { + done?: false + value: T + } + | { + done: true + value?: never + } + toArray(): T[] + one(): T + raw(): IterableIterator + columnNames: string[] + get rowsRead(): number + get rowsWritten(): number + [Symbol.iterator](): IterableIterator +} +interface Socket { + get readable(): ReadableStream + get writable(): WritableStream + get closed(): Promise + get opened(): Promise + get upgraded(): boolean + get secureTransport(): 'on' | 'off' | 'starttls' + close(): Promise + startTls(options?: TlsOptions): Socket +} +interface SocketOptions { + secureTransport?: string + allowHalfOpen: boolean + highWaterMark?: number | bigint +} +interface SocketAddress { + hostname: string + port: number +} +interface TlsOptions { + expectedServerHostname?: string +} +interface SocketInfo { + remoteAddress?: string + localAddress?: string +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource) */ +declare class EventSource extends EventTarget { + constructor(url: string, init?: EventSourceEventSourceInit) + /** + * Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close) + */ + close(): void + /** + * Returns the URL providing the event stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url) + */ + get url(): string + /** + * Returns true if the credentials mode for connection requests to the URL providing the event stream is set to "include", and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials) + */ + get withCredentials(): boolean + /** + * Returns the state of this EventSource object's connection. It can have the values described below. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState) + */ + get readyState(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + get onopen(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + set onopen(value: any | null) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + get onmessage(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + set onmessage(value: any | null) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + get onerror(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + set onerror(value: any | null) + static readonly CONNECTING: number + static readonly OPEN: number + static readonly CLOSED: number + static from(stream: ReadableStream): EventSource +} +interface EventSourceEventSourceInit { + withCredentials?: boolean + fetcher?: Fetcher +} +interface Container { + get running(): boolean + start(options?: ContainerStartupOptions): void + monitor(): Promise + destroy(error?: any): Promise + signal(signo: number): void + getTcpPort(port: number): Fetcher +} +interface ContainerStartupOptions { + entrypoint?: string[] + enableInternet: boolean + env?: Record +} +/** + * This Channel Messaging API interface represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort) + */ +interface MessagePort extends EventTarget { + /** + * Posts a message through the channel. Objects listed in transfer are transferred, not just cloned, meaning that they are no longer usable on the sending side. + * + * Throws a "DataCloneError" DOMException if transfer contains duplicate objects or port, or if message could not be cloned. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/postMessage) + */ + postMessage(data?: any, options?: any[] | MessagePortPostMessageOptions): void + /** + * Disconnects the port, so that it is no longer active. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/close) + */ + close(): void + /** + * Begins dispatching messages received on the port. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/start) + */ + start(): void + get onmessage(): any | null + set onmessage(value: any | null) +} +interface MessagePortPostMessageOptions { + transfer?: any[] +} +type LoopbackForExport< + T extends + | (new (...args: any[]) => Rpc.EntrypointBranded) + | ExportedHandler + | undefined = undefined, +> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded + ? LoopbackServiceStub> + : T extends new (...args: any[]) => Rpc.DurableObjectBranded + ? LoopbackDurableObjectClass> + : T extends ExportedHandler + ? LoopbackServiceStub + : undefined +type LoopbackServiceStub = + Fetcher & + (T extends CloudflareWorkersModule.WorkerEntrypoint + ? (opts: { props?: Props }) => Fetcher + : (opts: { props?: any }) => Fetcher) +type LoopbackDurableObjectClass = + DurableObjectClass & + (T extends CloudflareWorkersModule.DurableObject + ? (opts: { props?: Props }) => DurableObjectClass + : (opts: { props?: any }) => DurableObjectClass) +interface SyncKvStorage { + get(key: string): T | undefined + list(options?: SyncKvListOptions): Iterable<[string, T]> + put(key: string, value: T): void + delete(key: string): boolean +} +interface SyncKvListOptions { + start?: string + startAfter?: string + end?: string + prefix?: string + reverse?: boolean + limit?: number +} +interface WorkerStub { + getEntrypoint( + name?: string, + options?: WorkerStubEntrypointOptions + ): Fetcher +} +interface WorkerStubEntrypointOptions { + props?: any +} +interface WorkerLoader { + get( + name: string, + getCode: () => WorkerLoaderWorkerCode | Promise + ): WorkerStub +} +interface WorkerLoaderModule { + js?: string + cjs?: string + text?: string + data?: ArrayBuffer + json?: any + py?: string +} +interface WorkerLoaderWorkerCode { + compatibilityDate: string + compatibilityFlags?: string[] + allowExperimental?: boolean + mainModule: string + modules: Record + env?: any + globalOutbound?: Fetcher | null + tails?: Fetcher[] + streamingTails?: Fetcher[] +} +/** + * The Workers runtime supports a subset of the Performance API, used to measure timing and performance, + * as well as timing of subrequests and other operations. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) + */ +declare abstract class Performance { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */ + get timeOrigin(): number + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ + now(): number +} +type AiImageClassificationInput = { + image: number[] +} +type AiImageClassificationOutput = { + score?: number + label?: string +}[] +declare abstract class BaseAiImageClassification { + inputs: AiImageClassificationInput + postProcessedOutputs: AiImageClassificationOutput +} +type AiImageToTextInput = { + image: number[] + prompt?: string + max_tokens?: number + temperature?: number + top_p?: number + top_k?: number + seed?: number + repetition_penalty?: number + frequency_penalty?: number + presence_penalty?: number + raw?: boolean + messages?: RoleScopedChatInput[] +} +type AiImageToTextOutput = { + description: string +} +declare abstract class BaseAiImageToText { + inputs: AiImageToTextInput + postProcessedOutputs: AiImageToTextOutput +} +type AiImageTextToTextInput = { + image: string + prompt?: string + max_tokens?: number + temperature?: number + ignore_eos?: boolean + top_p?: number + top_k?: number + seed?: number + repetition_penalty?: number + frequency_penalty?: number + presence_penalty?: number + raw?: boolean + messages?: RoleScopedChatInput[] +} +type AiImageTextToTextOutput = { + description: string +} +declare abstract class BaseAiImageTextToText { + inputs: AiImageTextToTextInput + postProcessedOutputs: AiImageTextToTextOutput +} +type AiObjectDetectionInput = { + image: number[] +} +type AiObjectDetectionOutput = { + score?: number + label?: string +}[] +declare abstract class BaseAiObjectDetection { + inputs: AiObjectDetectionInput + postProcessedOutputs: AiObjectDetectionOutput +} +type AiSentenceSimilarityInput = { + source: string + sentences: string[] +} +type AiSentenceSimilarityOutput = number[] +declare abstract class BaseAiSentenceSimilarity { + inputs: AiSentenceSimilarityInput + postProcessedOutputs: AiSentenceSimilarityOutput +} +type AiAutomaticSpeechRecognitionInput = { + audio: number[] +} +type AiAutomaticSpeechRecognitionOutput = { + text?: string + words?: { + word: string + start: number + end: number + }[] + vtt?: string +} +declare abstract class BaseAiAutomaticSpeechRecognition { + inputs: AiAutomaticSpeechRecognitionInput + postProcessedOutputs: AiAutomaticSpeechRecognitionOutput +} +type AiSummarizationInput = { + input_text: string + max_length?: number +} +type AiSummarizationOutput = { + summary: string +} +declare abstract class BaseAiSummarization { + inputs: AiSummarizationInput + postProcessedOutputs: AiSummarizationOutput +} +type AiTextClassificationInput = { + text: string +} +type AiTextClassificationOutput = { + score?: number + label?: string +}[] +declare abstract class BaseAiTextClassification { + inputs: AiTextClassificationInput + postProcessedOutputs: AiTextClassificationOutput +} +type AiTextEmbeddingsInput = { + text: string | string[] +} +type AiTextEmbeddingsOutput = { + shape: number[] + data: number[][] +} +declare abstract class BaseAiTextEmbeddings { + inputs: AiTextEmbeddingsInput + postProcessedOutputs: AiTextEmbeddingsOutput +} +type RoleScopedChatInput = { + role: 'user' | 'assistant' | 'system' | 'tool' | (string & NonNullable) + content: string + name?: string +} +type AiTextGenerationToolLegacyInput = { + name: string + description: string + parameters?: { + type: 'object' | (string & NonNullable) + properties: { + [key: string]: { + type: string + description?: string + } + } + required: string[] + } +} +type AiTextGenerationToolInput = { + type: 'function' | (string & NonNullable) + function: { + name: string + description: string + parameters?: { + type: 'object' | (string & NonNullable) + properties: { + [key: string]: { + type: string + description?: string + } + } + required: string[] + } + } +} +type AiTextGenerationFunctionsInput = { + name: string + code: string +} +type AiTextGenerationResponseFormat = { + type: string + json_schema?: any +} +type AiTextGenerationInput = { + prompt?: string + raw?: boolean + stream?: boolean + max_tokens?: number + temperature?: number + top_p?: number + top_k?: number + seed?: number + repetition_penalty?: number + frequency_penalty?: number + presence_penalty?: number + messages?: RoleScopedChatInput[] + response_format?: AiTextGenerationResponseFormat + tools?: + | AiTextGenerationToolInput[] + | AiTextGenerationToolLegacyInput[] + | (object & NonNullable) + functions?: AiTextGenerationFunctionsInput[] +} +type AiTextGenerationOutput = { + response?: string + tool_calls?: { + name: string + arguments: unknown + }[] +} +declare abstract class BaseAiTextGeneration { + inputs: AiTextGenerationInput + postProcessedOutputs: AiTextGenerationOutput +} +type AiTextToSpeechInput = { + prompt: string + lang?: string +} +type AiTextToSpeechOutput = + | Uint8Array + | { + audio: string + } +declare abstract class BaseAiTextToSpeech { + inputs: AiTextToSpeechInput + postProcessedOutputs: AiTextToSpeechOutput +} +type AiTextToImageInput = { + prompt: string + negative_prompt?: string + height?: number + width?: number + image?: number[] + image_b64?: string + mask?: number[] + num_steps?: number + strength?: number + guidance?: number + seed?: number +} +type AiTextToImageOutput = ReadableStream +declare abstract class BaseAiTextToImage { + inputs: AiTextToImageInput + postProcessedOutputs: AiTextToImageOutput +} +type AiTranslationInput = { + text: string + target_lang: string + source_lang?: string +} +type AiTranslationOutput = { + translated_text?: string +} +declare abstract class BaseAiTranslation { + inputs: AiTranslationInput + postProcessedOutputs: AiTranslationOutput +} +type Ai_Cf_Baai_Bge_Base_En_V1_5_Input = + | { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + }[] + } +type Ai_Cf_Baai_Bge_Base_En_V1_5_Output = + | { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' + } + | AsyncResponse +interface AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string +} +declare abstract class Base_Ai_Cf_Baai_Bge_Base_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Output +} +type Ai_Cf_Openai_Whisper_Input = + | string + | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[] + } +interface Ai_Cf_Openai_Whisper_Output { + /** + * The transcription + */ + text: string + word_count?: number + words?: { + word?: string + /** + * The second this word begins in the recording + */ + start?: number + /** + * The ending second when the word completes + */ + end?: number + }[] + vtt?: string +} +declare abstract class Base_Ai_Cf_Openai_Whisper { + inputs: Ai_Cf_Openai_Whisper_Input + postProcessedOutputs: Ai_Cf_Openai_Whisper_Output +} +type Ai_Cf_Meta_M2M100_1_2B_Input = + | { + /** + * The text to be translated + */ + text: string + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + /** + * The text to be translated + */ + text: string + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string + }[] + } +type Ai_Cf_Meta_M2M100_1_2B_Output = + | { + /** + * The translated text in the target language + */ + translated_text?: string + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Meta_M2M100_1_2B { + inputs: Ai_Cf_Meta_M2M100_1_2B_Input + postProcessedOutputs: Ai_Cf_Meta_M2M100_1_2B_Output +} +type Ai_Cf_Baai_Bge_Small_En_V1_5_Input = + | { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + }[] + } +type Ai_Cf_Baai_Bge_Small_En_V1_5_Output = + | { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Baai_Bge_Small_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Output +} +type Ai_Cf_Baai_Bge_Large_En_V1_5_Input = + | { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + }[] + } +type Ai_Cf_Baai_Bge_Large_En_V1_5_Output = + | { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Baai_Bge_Large_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Output +} +type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input = + | string + | { + /** + * The input text prompt for the model to generate a response. + */ + prompt?: string + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number + image: number[] | (string & NonNullable) + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + } +interface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output { + description?: string +} +declare abstract class Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M { + inputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input + postProcessedOutputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output +} +type Ai_Cf_Openai_Whisper_Tiny_En_Input = + | string + | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[] + } +interface Ai_Cf_Openai_Whisper_Tiny_En_Output { + /** + * The transcription + */ + text: string + word_count?: number + words?: { + word?: string + /** + * The second this word begins in the recording + */ + start?: number + /** + * The ending second when the word completes + */ + end?: number + }[] + vtt?: string +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Tiny_En { + inputs: Ai_Cf_Openai_Whisper_Tiny_En_Input + postProcessedOutputs: Ai_Cf_Openai_Whisper_Tiny_En_Output +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input { + /** + * Base64 encoded value of the audio data. + */ + audio: string + /** + * Supported tasks are 'translate' or 'transcribe'. + */ + task?: string + /** + * The language of the audio being transcribed or translated. + */ + language?: string + /** + * Preprocess the audio with a voice activity detection model. + */ + vad_filter?: boolean + /** + * A text prompt to help provide context to the model on the contents of the audio. + */ + initial_prompt?: string + /** + * The prefix it appended the the beginning of the output of the transcription and can guide the transcription result. + */ + prefix?: string +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output { + transcription_info?: { + /** + * The language of the audio being transcribed or translated. + */ + language?: string + /** + * The confidence level or probability of the detected language being accurate, represented as a decimal between 0 and 1. + */ + language_probability?: number + /** + * The total duration of the original audio file, in seconds. + */ + duration?: number + /** + * The duration of the audio after applying Voice Activity Detection (VAD) to remove silent or irrelevant sections, in seconds. + */ + duration_after_vad?: number + } + /** + * The complete transcription of the audio. + */ + text: string + /** + * The total number of words in the transcription. + */ + word_count?: number + segments?: { + /** + * The starting time of the segment within the audio, in seconds. + */ + start?: number + /** + * The ending time of the segment within the audio, in seconds. + */ + end?: number + /** + * The transcription of the segment. + */ + text?: string + /** + * The temperature used in the decoding process, controlling randomness in predictions. Lower values result in more deterministic outputs. + */ + temperature?: number + /** + * The average log probability of the predictions for the words in this segment, indicating overall confidence. + */ + avg_logprob?: number + /** + * The compression ratio of the input to the output, measuring how much the text was compressed during the transcription process. + */ + compression_ratio?: number + /** + * The probability that the segment contains no speech, represented as a decimal between 0 and 1. + */ + no_speech_prob?: number + words?: { + /** + * The individual word transcribed from the audio. + */ + word?: string + /** + * The starting time of the word within the audio, in seconds. + */ + start?: number + /** + * The ending time of the word within the audio, in seconds. + */ + end?: number + }[] + }[] + /** + * The transcription in WebVTT format, which includes timing and text information for use in subtitles. + */ + vtt?: string +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo { + inputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input + postProcessedOutputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output +} +type Ai_Cf_Baai_Bge_M3_Input = + | BGEM3InputQueryAndContexts + | BGEM3InputEmbedding + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: (BGEM3InputQueryAndContexts1 | BGEM3InputEmbedding1)[] + } +interface BGEM3InputQueryAndContexts { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string + }[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +interface BGEM3InputEmbedding { + text: string | string[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +interface BGEM3InputQueryAndContexts1 { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string + }[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +interface BGEM3InputEmbedding1 { + text: string | string[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +type Ai_Cf_Baai_Bge_M3_Output = + | BGEM3OuputQuery + | BGEM3OutputEmbeddingForContexts + | BGEM3OuputEmbedding + | AsyncResponse +interface BGEM3OuputQuery { + response?: { + /** + * Index of the context in the request + */ + id?: number + /** + * Score of the context under the index. + */ + score?: number + }[] +} +interface BGEM3OutputEmbeddingForContexts { + response?: number[][] + shape?: number[] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' +} +interface BGEM3OuputEmbedding { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' +} +declare abstract class Base_Ai_Cf_Baai_Bge_M3 { + inputs: Ai_Cf_Baai_Bge_M3_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_M3_Output +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string + /** + * The number of diffusion steps; higher values can improve quality but take longer. + */ + steps?: number +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output { + /** + * The generated image in Base64 format. + */ + image?: string +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell { + inputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input = Prompt | Messages +interface Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + image?: number[] | (string & NonNullable) + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string +} +interface Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + image?: number[] | (string & NonNullable) + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * If true, the response will be streamed back incrementally. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = { + /** + * The generated text response from the model + */ + response?: string + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct { + inputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input = + | Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt + | Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages + | AsyncBatch +interface Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface JSONMode { + type?: 'json_object' | 'json_schema' + json_schema?: unknown +} +interface Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string + /** + * The content of the message as a string. + */ + content: string + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface AsyncBatch { + requests?: { + /** + * User-supplied reference. This field will be present in the response as well it can be used to reference the request and response. It's NOT validated to be unique. + */ + external_reference?: string + /** + * Prompt for the text generation model + */ + prompt?: string + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number + response_format?: JSONMode + }[] +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = + | { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast { + inputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Input { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender must alternate between 'user' and 'assistant'. + */ + role: 'user' | 'assistant' + /** + * The content of the message as a string. + */ + content: string + }[] + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Dictate the output format of the generated response. + */ + response_format?: { + /** + * Set to json_object to process and output generated text as JSON. + */ + type?: string + } +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Output { + response?: + | string + | { + /** + * Whether the conversation is safe or not. + */ + safe?: boolean + /** + * A list of what hazard categories predicted for the conversation, if the conversation is deemed unsafe. + */ + categories?: string[] + } + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } +} +declare abstract class Base_Ai_Cf_Meta_Llama_Guard_3_8B { + inputs: Ai_Cf_Meta_Llama_Guard_3_8B_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_Guard_3_8B_Output +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Input { + /** + * A query you wish to perform against the provided contexts. + */ + query: string + /** + * Number of returned results starting with the best score. + */ + top_k?: number + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string + }[] +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Output { + response?: { + /** + * Index of the context in the request + */ + id?: number + /** + * Score of the context under the index. + */ + score?: number + }[] +} +declare abstract class Base_Ai_Cf_Baai_Bge_Reranker_Base { + inputs: Ai_Cf_Baai_Bge_Reranker_Base_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Reranker_Base_Output +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input = + | Qwen2_5_Coder_32B_Instruct_Prompt + | Qwen2_5_Coder_32B_Instruct_Messages +interface Qwen2_5_Coder_32B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Qwen2_5_Coder_32B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string + /** + * The content of the message as a string. + */ + content: string + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct { + inputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input + postProcessedOutputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output +} +type Ai_Cf_Qwen_Qwq_32B_Input = Qwen_Qwq_32B_Prompt | Qwen_Qwq_32B_Messages +interface Qwen_Qwq_32B_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Qwen_Qwq_32B_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Qwen_Qwq_32B_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Qwen_Qwq_32B { + inputs: Ai_Cf_Qwen_Qwq_32B_Input + postProcessedOutputs: Ai_Cf_Qwen_Qwq_32B_Output +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input = + | Mistral_Small_3_1_24B_Instruct_Prompt + | Mistral_Small_3_1_24B_Instruct_Messages +interface Mistral_Small_3_1_24B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Mistral_Small_3_1_24B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct { + inputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input + postProcessedOutputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output +} +type Ai_Cf_Google_Gemma_3_12B_It_Input = + | Google_Gemma_3_12B_It_Prompt + | Google_Gemma_3_12B_It_Messages +interface Google_Gemma_3_12B_It_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Google_Gemma_3_12B_It_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Google_Gemma_3_12B_It_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Google_Gemma_3_12B_It { + inputs: Ai_Cf_Google_Gemma_3_12B_It_Input + postProcessedOutputs: Ai_Cf_Google_Gemma_3_12B_It_Output +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = + | Ai_Cf_Meta_Llama_4_Prompt + | Ai_Cf_Meta_Llama_4_Messages +interface Ai_Cf_Meta_Llama_4_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Ai_Cf_Meta_Llama_4_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + response_format?: JSONMode + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The tool call id. + */ + id?: string + /** + * Specifies the type of tool (e.g., 'function'). + */ + type?: string + /** + * Details of the function tool. + */ + function?: { + /** + * The name of the tool to be called + */ + name?: string + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + } + }[] +} +declare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct { + inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output +} +interface AiModels { + '@cf/huggingface/distilbert-sst-2-int8': BaseAiTextClassification + '@cf/stabilityai/stable-diffusion-xl-base-1.0': BaseAiTextToImage + '@cf/runwayml/stable-diffusion-v1-5-inpainting': BaseAiTextToImage + '@cf/runwayml/stable-diffusion-v1-5-img2img': BaseAiTextToImage + '@cf/lykon/dreamshaper-8-lcm': BaseAiTextToImage + '@cf/bytedance/stable-diffusion-xl-lightning': BaseAiTextToImage + '@cf/myshell-ai/melotts': BaseAiTextToSpeech + '@cf/microsoft/resnet-50': BaseAiImageClassification + '@cf/facebook/detr-resnet-50': BaseAiObjectDetection + '@cf/meta/llama-2-7b-chat-int8': BaseAiTextGeneration + '@cf/mistral/mistral-7b-instruct-v0.1': BaseAiTextGeneration + '@cf/meta/llama-2-7b-chat-fp16': BaseAiTextGeneration + '@hf/thebloke/llama-2-13b-chat-awq': BaseAiTextGeneration + '@hf/thebloke/mistral-7b-instruct-v0.1-awq': BaseAiTextGeneration + '@hf/thebloke/zephyr-7b-beta-awq': BaseAiTextGeneration + '@hf/thebloke/openhermes-2.5-mistral-7b-awq': BaseAiTextGeneration + '@hf/thebloke/neural-chat-7b-v3-1-awq': BaseAiTextGeneration + '@hf/thebloke/llamaguard-7b-awq': BaseAiTextGeneration + '@hf/thebloke/deepseek-coder-6.7b-base-awq': BaseAiTextGeneration + '@hf/thebloke/deepseek-coder-6.7b-instruct-awq': BaseAiTextGeneration + '@cf/deepseek-ai/deepseek-math-7b-instruct': BaseAiTextGeneration + '@cf/defog/sqlcoder-7b-2': BaseAiTextGeneration + '@cf/openchat/openchat-3.5-0106': BaseAiTextGeneration + '@cf/tiiuae/falcon-7b-instruct': BaseAiTextGeneration + '@cf/thebloke/discolm-german-7b-v1-awq': BaseAiTextGeneration + '@cf/qwen/qwen1.5-0.5b-chat': BaseAiTextGeneration + '@cf/qwen/qwen1.5-7b-chat-awq': BaseAiTextGeneration + '@cf/qwen/qwen1.5-14b-chat-awq': BaseAiTextGeneration + '@cf/tinyllama/tinyllama-1.1b-chat-v1.0': BaseAiTextGeneration + '@cf/microsoft/phi-2': BaseAiTextGeneration + '@cf/qwen/qwen1.5-1.8b-chat': BaseAiTextGeneration + '@cf/mistral/mistral-7b-instruct-v0.2-lora': BaseAiTextGeneration + '@hf/nousresearch/hermes-2-pro-mistral-7b': BaseAiTextGeneration + '@hf/nexusflow/starling-lm-7b-beta': BaseAiTextGeneration + '@hf/google/gemma-7b-it': BaseAiTextGeneration + '@cf/meta-llama/llama-2-7b-chat-hf-lora': BaseAiTextGeneration + '@cf/google/gemma-2b-it-lora': BaseAiTextGeneration + '@cf/google/gemma-7b-it-lora': BaseAiTextGeneration + '@hf/mistral/mistral-7b-instruct-v0.2': BaseAiTextGeneration + '@cf/meta/llama-3-8b-instruct': BaseAiTextGeneration + '@cf/fblgit/una-cybertron-7b-v2-bf16': BaseAiTextGeneration + '@cf/meta/llama-3-8b-instruct-awq': BaseAiTextGeneration + '@hf/meta-llama/meta-llama-3-8b-instruct': BaseAiTextGeneration + '@cf/meta/llama-3.1-8b-instruct': BaseAiTextGeneration + '@cf/meta/llama-3.1-8b-instruct-fp8': BaseAiTextGeneration + '@cf/meta/llama-3.1-8b-instruct-awq': BaseAiTextGeneration + '@cf/meta/llama-3.2-3b-instruct': BaseAiTextGeneration + '@cf/meta/llama-3.2-1b-instruct': BaseAiTextGeneration + '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b': BaseAiTextGeneration + '@cf/facebook/bart-large-cnn': BaseAiSummarization + '@cf/llava-hf/llava-1.5-7b-hf': BaseAiImageToText + '@cf/baai/bge-base-en-v1.5': Base_Ai_Cf_Baai_Bge_Base_En_V1_5 + '@cf/openai/whisper': Base_Ai_Cf_Openai_Whisper + '@cf/meta/m2m100-1.2b': Base_Ai_Cf_Meta_M2M100_1_2B + '@cf/baai/bge-small-en-v1.5': Base_Ai_Cf_Baai_Bge_Small_En_V1_5 + '@cf/baai/bge-large-en-v1.5': Base_Ai_Cf_Baai_Bge_Large_En_V1_5 + '@cf/unum/uform-gen2-qwen-500m': Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M + '@cf/openai/whisper-tiny-en': Base_Ai_Cf_Openai_Whisper_Tiny_En + '@cf/openai/whisper-large-v3-turbo': Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo + '@cf/baai/bge-m3': Base_Ai_Cf_Baai_Bge_M3 + '@cf/black-forest-labs/flux-1-schnell': Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell + '@cf/meta/llama-3.2-11b-vision-instruct': Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct + '@cf/meta/llama-3.3-70b-instruct-fp8-fast': Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast + '@cf/meta/llama-guard-3-8b': Base_Ai_Cf_Meta_Llama_Guard_3_8B + '@cf/baai/bge-reranker-base': Base_Ai_Cf_Baai_Bge_Reranker_Base + '@cf/qwen/qwen2.5-coder-32b-instruct': Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct + '@cf/qwen/qwq-32b': Base_Ai_Cf_Qwen_Qwq_32B + '@cf/mistralai/mistral-small-3.1-24b-instruct': Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct + '@cf/google/gemma-3-12b-it': Base_Ai_Cf_Google_Gemma_3_12B_It + '@cf/meta/llama-4-scout-17b-16e-instruct': Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct +} +type AiOptions = { + /** + * Send requests as an asynchronous batch job, only works for supported models + * https://developers.cloudflare.com/workers-ai/features/batch-api + */ + queueRequest?: boolean + gateway?: GatewayOptions + returnRawResponse?: boolean + prefix?: string + extraHeaders?: object +} +type ConversionResponse = { + name: string + mimeType: string + format: 'markdown' + tokens: number + data: string +} +type AiModelsSearchParams = { + author?: string + hide_experimental?: boolean + page?: number + per_page?: number + search?: string + source?: number + task?: string +} +type AiModelsSearchObject = { + id: string + source: number + name: string + description: string + task: { + id: string + name: string + description: string + } + tags: string[] + properties: { + property_id: string + value: string + }[] +} +interface InferenceUpstreamError extends Error {} +interface AiInternalError extends Error {} +type AiModelListType = Record +declare abstract class Ai { + aiGatewayLogId: string | null + gateway(gatewayId: string): AiGateway + autorag(autoragId?: string): AutoRAG + run< + Name extends keyof AiModelList, + Options extends AiOptions, + InputOptions extends AiModelList[Name]['inputs'], + >( + model: Name, + inputs: InputOptions, + options?: Options + ): Promise< + Options extends { + returnRawResponse: true + } + ? Response + : InputOptions extends { + stream: true + } + ? ReadableStream + : AiModelList[Name]['postProcessedOutputs'] + > + models(params?: AiModelsSearchParams): Promise + toMarkdown( + files: { + name: string + blob: Blob + }[], + options?: { + gateway?: GatewayOptions + extraHeaders?: object + } + ): Promise + toMarkdown( + files: { + name: string + blob: Blob + }, + options?: { + gateway?: GatewayOptions + extraHeaders?: object + } + ): Promise +} +type GatewayRetries = { + maxAttempts?: 1 | 2 | 3 | 4 | 5 + retryDelayMs?: number + backoff?: 'constant' | 'linear' | 'exponential' +} +type GatewayOptions = { + id: string + cacheKey?: string + cacheTtl?: number + skipCache?: boolean + metadata?: Record + collectLog?: boolean + eventId?: string + requestTimeoutMs?: number + retries?: GatewayRetries +} +type UniversalGatewayOptions = Exclude & { + /** + ** @deprecated + */ + id?: string +} +type AiGatewayPatchLog = { + score?: number | null + feedback?: -1 | 1 | null + metadata?: Record | null +} +type AiGatewayLog = { + id: string + provider: string + model: string + model_type?: string + path: string + duration: number + request_type?: string + request_content_type?: string + status_code: number + response_content_type?: string + success: boolean + cached: boolean + tokens_in?: number + tokens_out?: number + metadata?: Record + step?: number + cost?: number + custom_cost?: boolean + request_size: number + request_head?: string + request_head_complete: boolean + response_size: number + response_head?: string + response_head_complete: boolean + created_at: Date +} +type AIGatewayProviders = + | 'workers-ai' + | 'anthropic' + | 'aws-bedrock' + | 'azure-openai' + | 'google-vertex-ai' + | 'huggingface' + | 'openai' + | 'perplexity-ai' + | 'replicate' + | 'groq' + | 'cohere' + | 'google-ai-studio' + | 'mistral' + | 'grok' + | 'openrouter' + | 'deepseek' + | 'cerebras' + | 'cartesia' + | 'elevenlabs' + | 'adobe-firefly' +type AIGatewayHeaders = { + 'cf-aig-metadata': Record | string + 'cf-aig-custom-cost': + | { + per_token_in?: number + per_token_out?: number + } + | { + total_cost?: number + } + | string + 'cf-aig-cache-ttl': number | string + 'cf-aig-skip-cache': boolean | string + 'cf-aig-cache-key': string + 'cf-aig-event-id': string + 'cf-aig-request-timeout': number | string + 'cf-aig-max-attempts': number | string + 'cf-aig-retry-delay': number | string + 'cf-aig-backoff': string + 'cf-aig-collect-log': boolean | string + Authorization: string + 'Content-Type': string + [key: string]: string | number | boolean | object +} +type AIGatewayUniversalRequest = { + provider: AIGatewayProviders | string // eslint-disable-line + endpoint: string + headers: Partial + query: unknown +} +interface AiGatewayInternalError extends Error {} +interface AiGatewayLogNotFound extends Error {} +declare abstract class AiGateway { + patchLog(logId: string, data: AiGatewayPatchLog): Promise + getLog(logId: string): Promise + run( + data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], + options?: { + gateway?: UniversalGatewayOptions + extraHeaders?: object + } + ): Promise + getUrl(provider?: AIGatewayProviders | string): Promise // eslint-disable-line +} +interface AutoRAGInternalError extends Error {} +interface AutoRAGNotFoundError extends Error {} +interface AutoRAGUnauthorizedError extends Error {} +interface AutoRAGNameNotSetError extends Error {} +type ComparisonFilter = { + key: string + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' + value: string | number | boolean +} +type CompoundFilter = { + type: 'and' | 'or' + filters: ComparisonFilter[] +} +type AutoRagSearchRequest = { + query: string + filters?: CompoundFilter | ComparisonFilter + max_num_results?: number + ranking_options?: { + ranker?: string + score_threshold?: number + } + rewrite_query?: boolean +} +type AutoRagAiSearchRequest = AutoRagSearchRequest & { + stream?: boolean + system_prompt?: string +} +type AutoRagAiSearchRequestStreaming = Omit & { + stream: true +} +type AutoRagSearchResponse = { + object: 'vector_store.search_results.page' + search_query: string + data: { + file_id: string + filename: string + score: number + attributes: Record + content: { + type: 'text' + text: string + }[] + }[] + has_more: boolean + next_page: string | null +} +type AutoRagListResponse = { + id: string + enable: boolean + type: string + source: string + vectorize_name: string + paused: boolean + status: string +}[] +type AutoRagAiSearchResponse = AutoRagSearchResponse & { + response: string +} +declare abstract class AutoRAG { + list(): Promise + search(params: AutoRagSearchRequest): Promise + aiSearch(params: AutoRagAiSearchRequestStreaming): Promise + aiSearch(params: AutoRagAiSearchRequest): Promise + aiSearch(params: AutoRagAiSearchRequest): Promise +} +interface BasicImageTransformations { + /** + * Maximum width in image pixels. The value must be an integer. + */ + width?: number + /** + * Maximum height in image pixels. The value must be an integer. + */ + height?: number + /** + * Resizing mode as a string. It affects interpretation of width and height + * options: + * - scale-down: Similar to contain, but the image is never enlarged. If + * the image is larger than given width or height, it will be resized. + * Otherwise its original size will be kept. + * - contain: Resizes to maximum size that fits within the given width and + * height. If only a single dimension is given (e.g. only width), the + * image will be shrunk or enlarged to exactly match that dimension. + * Aspect ratio is always preserved. + * - cover: Resizes (shrinks or enlarges) to fill the entire area of width + * and height. If the image has an aspect ratio different from the ratio + * of width and height, it will be cropped to fit. + * - crop: The image will be shrunk and cropped to fit within the area + * specified by width and height. The image will not be enlarged. For images + * smaller than the given dimensions it's the same as scale-down. For + * images larger than the given dimensions, it's the same as cover. + * See also trim. + * - pad: Resizes to the maximum size that fits within the given width and + * height, and then fills the remaining area with a background color + * (white by default). Use of this mode is not recommended, as the same + * effect can be more efficiently achieved with the contain mode and the + * CSS object-fit: contain property. + * - squeeze: Stretches and deforms to the width and height given, even if it + * breaks aspect ratio + */ + fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad' | 'squeeze' + /** + * Image segmentation using artificial intelligence models. Sets pixels not + * within selected segment area to transparent e.g "foreground" sets every + * background pixel as transparent. + */ + segment?: 'foreground' + /** + * When cropping with fit: "cover", this defines the side or point that should + * be left uncropped. The value is either a string + * "left", "right", "top", "bottom", "auto", or "center" (the default), + * or an object {x, y} containing focal point coordinates in the original + * image expressed as fractions ranging from 0.0 (top or left) to 1.0 + * (bottom or right), 0.5 being the center. {fit: "cover", gravity: "top"} will + * crop bottom or left and right sides as necessary, but won’t crop anything + * from the top. {fit: "cover", gravity: {x:0.5, y:0.2}} will crop each side to + * preserve as much as possible around a point at 20% of the height of the + * source image. + */ + gravity?: + | 'face' + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'center' + | 'auto' + | 'entropy' + | BasicImageTransformationsGravityCoordinates + /** + * Background color to add underneath the image. Applies only to images with + * transparency (such as PNG). Accepts any CSS color (#RRGGBB, rgba(…), + * hsl(…), etc.) + */ + background?: string + /** + * Number of degrees (90, 180, 270) to rotate the image by. width and height + * options refer to axes after rotation. + */ + rotate?: 0 | 90 | 180 | 270 | 360 +} +interface BasicImageTransformationsGravityCoordinates { + x?: number + y?: number + mode?: 'remainder' | 'box-center' +} +/** + * In addition to the properties you can set in the RequestInit dict + * that you pass as an argument to the Request constructor, you can + * set certain properties of a `cf` object to control how Cloudflare + * features are applied to that new Request. + * + * Note: Currently, these properties cannot be tested in the + * playground. + */ +interface RequestInitCfProperties extends Record { + cacheEverything?: boolean + /** + * A request's cache key is what determines if two requests are + * "the same" for caching purposes. If a request has the same cache key + * as some previous request, then we can serve the same cached response for + * both. (e.g. 'some-key') + * + * Only available for Enterprise customers. + */ + cacheKey?: string + /** + * This allows you to append additional Cache-Tag response headers + * to the origin response without modifications to the origin server. + * This will allow for greater control over the Purge by Cache Tag feature + * utilizing changes only in the Workers process. + * + * Only available for Enterprise customers. + */ + cacheTags?: string[] + /** + * Force response to be cached for a given number of seconds. (e.g. 300) + */ + cacheTtl?: number + /** + * Force response to be cached for a given number of seconds based on the Origin status code. + * (e.g. { '200-299': 86400, '404': 1, '500-599': 0 }) + */ + cacheTtlByStatus?: Record + scrapeShield?: boolean + apps?: boolean + image?: RequestInitCfPropertiesImage + minify?: RequestInitCfPropertiesImageMinify + mirage?: boolean + polish?: 'lossy' | 'lossless' | 'off' + r2?: RequestInitCfPropertiesR2 + /** + * Redirects the request to an alternate origin server. You can use this, + * for example, to implement load balancing across several origins. + * (e.g.us-east.example.com) + * + * Note - For security reasons, the hostname set in resolveOverride must + * be proxied on the same Cloudflare zone of the incoming request. + * Otherwise, the setting is ignored. CNAME hosts are allowed, so to + * resolve to a host under a different domain or a DNS only domain first + * declare a CNAME record within your own zone’s DNS mapping to the + * external hostname, set proxy on Cloudflare, then set resolveOverride + * to point to that CNAME record. + */ + resolveOverride?: string +} +interface RequestInitCfPropertiesImageDraw extends BasicImageTransformations { + /** + * Absolute URL of the image file to use for the drawing. It can be any of + * the supported file formats. For drawing of watermarks or non-rectangular + * overlays we recommend using PNG or WebP images. + */ + url: string + /** + * Floating-point number between 0 (transparent) and 1 (opaque). + * For example, opacity: 0.5 makes overlay semitransparent. + */ + opacity?: number + /** + * - If set to true, the overlay image will be tiled to cover the entire + * area. This is useful for stock-photo-like watermarks. + * - If set to "x", the overlay image will be tiled horizontally only + * (form a line). + * - If set to "y", the overlay image will be tiled vertically only + * (form a line). + */ + repeat?: true | 'x' | 'y' + /** + * Position of the overlay image relative to a given edge. Each property is + * an offset in pixels. 0 aligns exactly to the edge. For example, left: 10 + * positions left side of the overlay 10 pixels from the left edge of the + * image it's drawn over. bottom: 0 aligns bottom of the overlay with bottom + * of the background image. + * + * Setting both left & right, or both top & bottom is an error. + * + * If no position is specified, the image will be centered. + */ + top?: number + left?: number + bottom?: number + right?: number +} +interface RequestInitCfPropertiesImage extends BasicImageTransformations { + /** + * Device Pixel Ratio. Default 1. Multiplier for width/height that makes it + * easier to specify higher-DPI sizes in . + */ + dpr?: number + /** + * Allows you to trim your image. Takes dpr into account and is performed before + * resizing or rotation. + * + * It can be used as: + * - left, top, right, bottom - it will specify the number of pixels to cut + * off each side + * - width, height - the width/height you'd like to end up with - can be used + * in combination with the properties above + * - border - this will automatically trim the surroundings of an image based on + * it's color. It consists of three properties: + * - color: rgb or hex representation of the color you wish to trim (todo: verify the rgba bit) + * - tolerance: difference from color to treat as color + * - keep: the number of pixels of border to keep + */ + trim?: + | 'border' + | { + top?: number + bottom?: number + left?: number + right?: number + width?: number + height?: number + border?: + | boolean + | { + color?: string + tolerance?: number + keep?: number + } + } + /** + * Quality setting from 1-100 (useful values are in 60-90 range). Lower values + * make images look worse, but load faster. The default is 85. It applies only + * to JPEG and WebP images. It doesn’t have any effect on PNG. + */ + quality?: number | 'low' | 'medium-low' | 'medium-high' | 'high' + /** + * Output format to generate. It can be: + * - avif: generate images in AVIF format. + * - webp: generate images in Google WebP format. Set quality to 100 to get + * the WebP-lossless format. + * - json: instead of generating an image, outputs information about the + * image, in JSON format. The JSON object will contain image size + * (before and after resizing), source image’s MIME type, file size, etc. + * - jpeg: generate images in JPEG format. + * - png: generate images in PNG format. + */ + format?: 'avif' | 'webp' | 'json' | 'jpeg' | 'png' | 'baseline-jpeg' | 'png-force' | 'svg' + /** + * Whether to preserve animation frames from input files. Default is true. + * Setting it to false reduces animations to still images. This setting is + * recommended when enlarging images or processing arbitrary user content, + * because large GIF animations can weigh tens or even hundreds of megabytes. + * It is also useful to set anim:false when using format:"json" to get the + * response quicker without the number of frames. + */ + anim?: boolean + /** + * What EXIF data should be preserved in the output image. Note that EXIF + * rotation and embedded color profiles are always applied ("baked in" into + * the image), and aren't affected by this option. Note that if the Polish + * feature is enabled, all metadata may have been removed already and this + * option may have no effect. + * - keep: Preserve most of EXIF metadata, including GPS location if there's + * any. + * - copyright: Only keep the copyright tag, and discard everything else. + * This is the default behavior for JPEG files. + * - none: Discard all invisible EXIF metadata. Currently WebP and PNG + * output formats always discard metadata. + */ + metadata?: 'keep' | 'copyright' | 'none' + /** + * Strength of sharpening filter to apply to the image. Floating-point + * number between 0 (no sharpening, default) and 10 (maximum). 1.0 is a + * recommended value for downscaled images. + */ + sharpen?: number + /** + * Radius of a blur filter (approximate gaussian). Maximum supported radius + * is 250. + */ + blur?: number + /** + * Overlays are drawn in the order they appear in the array (last array + * entry is the topmost layer). + */ + draw?: RequestInitCfPropertiesImageDraw[] + /** + * Fetching image from authenticated origin. Setting this property will + * pass authentication headers (Authorization, Cookie, etc.) through to + * the origin. + */ + 'origin-auth'?: 'share-publicly' + /** + * Adds a border around the image. The border is added after resizing. Border + * width takes dpr into account, and can be specified either using a single + * width property, or individually for each side. + */ + border?: + | { + color: string + width: number + } + | { + color: string + top: number + right: number + bottom: number + left: number + } + /** + * Increase brightness by a factor. A value of 1.0 equals no change, a value + * of 0.5 equals half brightness, and a value of 2.0 equals twice as bright. + * 0 is ignored. + */ + brightness?: number + /** + * Increase contrast by a factor. A value of 1.0 equals no change, a value of + * 0.5 equals low contrast, and a value of 2.0 equals high contrast. 0 is + * ignored. + */ + contrast?: number + /** + * Increase exposure by a factor. A value of 1.0 equals no change, a value of + * 0.5 darkens the image, and a value of 2.0 lightens the image. 0 is ignored. + */ + gamma?: number + /** + * Increase contrast by a factor. A value of 1.0 equals no change, a value of + * 0.5 equals low contrast, and a value of 2.0 equals high contrast. 0 is + * ignored. + */ + saturation?: number + /** + * Flips the images horizontally, vertically, or both. Flipping is applied before + * rotation, so if you apply flip=h,rotate=90 then the image will be flipped + * horizontally, then rotated by 90 degrees. + */ + flip?: 'h' | 'v' | 'hv' + /** + * Slightly reduces latency on a cache miss by selecting a + * quickest-to-compress file format, at a cost of increased file size and + * lower image quality. It will usually override the format option and choose + * JPEG over WebP or AVIF. We do not recommend using this option, except in + * unusual circumstances like resizing uncacheable dynamically-generated + * images. + */ + compression?: 'fast' +} +interface RequestInitCfPropertiesImageMinify { + javascript?: boolean + css?: boolean + html?: boolean +} +interface RequestInitCfPropertiesR2 { + /** + * Colo id of bucket that an object is stored in + */ + bucketColoId?: number +} +/** + * Request metadata provided by Cloudflare's edge. + */ +type IncomingRequestCfProperties = IncomingRequestCfPropertiesBase & + IncomingRequestCfPropertiesBotManagementEnterprise & + IncomingRequestCfPropertiesCloudflareForSaaSEnterprise & + IncomingRequestCfPropertiesGeographicInformation & + IncomingRequestCfPropertiesCloudflareAccessOrApiShield +interface IncomingRequestCfPropertiesBase extends Record { + /** + * [ASN](https://www.iana.org/assignments/as-numbers/as-numbers.xhtml) of the incoming request. + * + * @example 395747 + */ + asn?: number + /** + * The organization which owns the ASN of the incoming request. + * + * @example "Google Cloud" + */ + asOrganization?: string + /** + * The original value of the `Accept-Encoding` header if Cloudflare modified it. + * + * @example "gzip, deflate, br" + */ + clientAcceptEncoding?: string + /** + * The number of milliseconds it took for the request to reach your worker. + * + * @example 22 + */ + clientTcpRtt?: number + /** + * The three-letter [IATA](https://en.wikipedia.org/wiki/IATA_airport_code) + * airport code of the data center that the request hit. + * + * @example "DFW" + */ + colo: string + /** + * Represents the upstream's response to a + * [TCP `keepalive` message](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) + * from cloudflare. + * + * For workers with no upstream, this will always be `1`. + * + * @example 3 + */ + edgeRequestKeepAliveStatus: IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus + /** + * The HTTP Protocol the request used. + * + * @example "HTTP/2" + */ + httpProtocol: string + /** + * The browser-requested prioritization information in the request object. + * + * If no information was set, defaults to the empty string `""` + * + * @example "weight=192;exclusive=0;group=3;group-weight=127" + * @default "" + */ + requestPriority: string + /** + * The TLS version of the connection to Cloudflare. + * In requests served over plaintext (without TLS), this property is the empty string `""`. + * + * @example "TLSv1.3" + */ + tlsVersion: string + /** + * The cipher for the connection to Cloudflare. + * In requests served over plaintext (without TLS), this property is the empty string `""`. + * + * @example "AEAD-AES128-GCM-SHA256" + */ + tlsCipher: string + /** + * Metadata containing the [`HELLO`](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2) and [`FINISHED`](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9) messages from this request's TLS handshake. + * + * If the incoming request was served over plaintext (without TLS) this field is undefined. + */ + tlsExportedAuthenticator?: IncomingRequestCfPropertiesExportedAuthenticatorMetadata +} +interface IncomingRequestCfPropertiesBotManagementBase { + /** + * Cloudflare’s [level of certainty](https://developers.cloudflare.com/bots/concepts/bot-score/) that a request comes from a bot, + * represented as an integer percentage between `1` (almost certainly a bot) and `99` (almost certainly human). + * + * @example 54 + */ + score: number + /** + * A boolean value that is true if the request comes from a good bot, like Google or Bing. + * Most customers choose to allow this traffic. For more details, see [Traffic from known bots](https://developers.cloudflare.com/firewall/known-issues-and-faq/#how-does-firewall-rules-handle-traffic-from-known-bots). + */ + verifiedBot: boolean + /** + * A boolean value that is true if the request originates from a + * Cloudflare-verified proxy service. + */ + corporateProxy: boolean + /** + * A boolean value that's true if the request matches [file extensions](https://developers.cloudflare.com/bots/reference/static-resources/) for many types of static resources. + */ + staticResource: boolean + /** + * List of IDs that correlate to the Bot Management heuristic detections made on a request (you can have multiple heuristic detections on the same request). + */ + detectionIds: number[] +} +interface IncomingRequestCfPropertiesBotManagement { + /** + * Results of Cloudflare's Bot Management analysis + */ + botManagement: IncomingRequestCfPropertiesBotManagementBase + /** + * Duplicate of `botManagement.score`. + * + * @deprecated + */ + clientTrustScore: number +} +interface IncomingRequestCfPropertiesBotManagementEnterprise extends IncomingRequestCfPropertiesBotManagement { + /** + * Results of Cloudflare's Bot Management analysis + */ + botManagement: IncomingRequestCfPropertiesBotManagementBase & { + /** + * A [JA3 Fingerprint](https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/) to help profile specific SSL/TLS clients + * across different destination IPs, Ports, and X509 certificates. + */ + ja3Hash: string + } +} +interface IncomingRequestCfPropertiesCloudflareForSaaSEnterprise { + /** + * Custom metadata set per-host in [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/). + * + * This field is only present if you have Cloudflare for SaaS enabled on your account + * and you have followed the [required steps to enable it]((https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata/)). + */ + hostMetadata?: HostMetadata +} +interface IncomingRequestCfPropertiesCloudflareAccessOrApiShield { + /** + * Information about the client certificate presented to Cloudflare. + * + * This is populated when the incoming request is served over TLS using + * either Cloudflare Access or API Shield (mTLS) + * and the presented SSL certificate has a valid + * [Certificate Serial Number](https://ldapwiki.com/wiki/Certificate%20Serial%20Number) + * (i.e., not `null` or `""`). + * + * Otherwise, a set of placeholder values are used. + * + * The property `certPresented` will be set to `"1"` when + * the object is populated (i.e. the above conditions were met). + */ + tlsClientAuth: + | IncomingRequestCfPropertiesTLSClientAuth + | IncomingRequestCfPropertiesTLSClientAuthPlaceholder +} +/** + * Metadata about the request's TLS handshake + */ +interface IncomingRequestCfPropertiesExportedAuthenticatorMetadata { + /** + * The client's [`HELLO` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2), encoded in hexadecimal + * + * @example "44372ba35fa1270921d318f34c12f155dc87b682cf36a790cfaa3ba8737a1b5d" + */ + clientHandshake: string + /** + * The server's [`HELLO` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2), encoded in hexadecimal + * + * @example "44372ba35fa1270921d318f34c12f155dc87b682cf36a790cfaa3ba8737a1b5d" + */ + serverHandshake: string + /** + * The client's [`FINISHED` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9), encoded in hexadecimal + * + * @example "084ee802fe1348f688220e2a6040a05b2199a761f33cf753abb1b006792d3f8b" + */ + clientFinished: string + /** + * The server's [`FINISHED` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9), encoded in hexadecimal + * + * @example "084ee802fe1348f688220e2a6040a05b2199a761f33cf753abb1b006792d3f8b" + */ + serverFinished: string +} +/** + * Geographic data about the request's origin. + */ +interface IncomingRequestCfPropertiesGeographicInformation { + /** + * The [ISO 3166-1 Alpha 2](https://www.iso.org/iso-3166-country-codes.html) country code the request originated from. + * + * If your worker is [configured to accept TOR connections](https://support.cloudflare.com/hc/en-us/articles/203306930-Understanding-Cloudflare-Tor-support-and-Onion-Routing), this may also be `"T1"`, indicating a request that originated over TOR. + * + * If Cloudflare is unable to determine where the request originated this property is omitted. + * + * The country code `"T1"` is used for requests originating on TOR. + * + * @example "GB" + */ + country?: Iso3166Alpha2Code | 'T1' + /** + * If present, this property indicates that the request originated in the EU + * + * @example "1" + */ + isEUCountry?: '1' + /** + * A two-letter code indicating the continent the request originated from. + * + * @example "AN" + */ + continent?: ContinentCode + /** + * The city the request originated from + * + * @example "Austin" + */ + city?: string + /** + * Postal code of the incoming request + * + * @example "78701" + */ + postalCode?: string + /** + * Latitude of the incoming request + * + * @example "30.27130" + */ + latitude?: string + /** + * Longitude of the incoming request + * + * @example "-97.74260" + */ + longitude?: string + /** + * Timezone of the incoming request + * + * @example "America/Chicago" + */ + timezone?: string + /** + * If known, the ISO 3166-2 name for the first level region associated with + * the IP address of the incoming request + * + * @example "Texas" + */ + region?: string + /** + * If known, the ISO 3166-2 code for the first-level region associated with + * the IP address of the incoming request + * + * @example "TX" + */ + regionCode?: string + /** + * Metro code (DMA) of the incoming request + * + * @example "635" + */ + metroCode?: string +} +/** Data about the incoming request's TLS certificate */ +interface IncomingRequestCfPropertiesTLSClientAuth { + /** Always `"1"`, indicating that the certificate was presented */ + certPresented: '1' + /** + * Result of certificate verification. + * + * @example "FAILED:self signed certificate" + */ + certVerified: Exclude + /** The presented certificate's revokation status. + * + * - A value of `"1"` indicates the certificate has been revoked + * - A value of `"0"` indicates the certificate has not been revoked + */ + certRevoked: '1' | '0' + /** + * The certificate issuer's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) + * + * @example "CN=cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certIssuerDN: string + /** + * The certificate subject's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) + * + * @example "CN=*.cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certSubjectDN: string + /** + * The certificate issuer's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) ([RFC 2253](https://www.rfc-editor.org/rfc/rfc2253.html) formatted) + * + * @example "CN=cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certIssuerDNRFC2253: string + /** + * The certificate subject's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) ([RFC 2253](https://www.rfc-editor.org/rfc/rfc2253.html) formatted) + * + * @example "CN=*.cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certSubjectDNRFC2253: string + /** The certificate issuer's distinguished name (legacy policies) */ + certIssuerDNLegacy: string + /** The certificate subject's distinguished name (legacy policies) */ + certSubjectDNLegacy: string + /** + * The certificate's serial number + * + * @example "00936EACBE07F201DF" + */ + certSerial: string + /** + * The certificate issuer's serial number + * + * @example "2489002934BDFEA34" + */ + certIssuerSerial: string + /** + * The certificate's Subject Key Identifier + * + * @example "BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4" + */ + certSKI: string + /** + * The certificate issuer's Subject Key Identifier + * + * @example "BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4" + */ + certIssuerSKI: string + /** + * The certificate's SHA-1 fingerprint + * + * @example "6b9109f323999e52259cda7373ff0b4d26bd232e" + */ + certFingerprintSHA1: string + /** + * The certificate's SHA-256 fingerprint + * + * @example "acf77cf37b4156a2708e34c4eb755f9b5dbbe5ebb55adfec8f11493438d19e6ad3f157f81fa3b98278453d5652b0c1fd1d71e5695ae4d709803a4d3f39de9dea" + */ + certFingerprintSHA256: string + /** + * The effective starting date of the certificate + * + * @example "Dec 22 19:39:00 2018 GMT" + */ + certNotBefore: string + /** + * The effective expiration date of the certificate + * + * @example "Dec 22 19:39:00 2018 GMT" + */ + certNotAfter: string +} +/** Placeholder values for TLS Client Authorization */ +interface IncomingRequestCfPropertiesTLSClientAuthPlaceholder { + certPresented: '0' + certVerified: 'NONE' + certRevoked: '0' + certIssuerDN: '' + certSubjectDN: '' + certIssuerDNRFC2253: '' + certSubjectDNRFC2253: '' + certIssuerDNLegacy: '' + certSubjectDNLegacy: '' + certSerial: '' + certIssuerSerial: '' + certSKI: '' + certIssuerSKI: '' + certFingerprintSHA1: '' + certFingerprintSHA256: '' + certNotBefore: '' + certNotAfter: '' +} +/** Possible outcomes of TLS verification */ +declare type CertVerificationStatus = + /** Authentication succeeded */ + | 'SUCCESS' + /** No certificate was presented */ + | 'NONE' + /** Failed because the certificate was self-signed */ + | 'FAILED:self signed certificate' + /** Failed because the certificate failed a trust chain check */ + | 'FAILED:unable to verify the first certificate' + /** Failed because the certificate not yet valid */ + | 'FAILED:certificate is not yet valid' + /** Failed because the certificate is expired */ + | 'FAILED:certificate has expired' + /** Failed for another unspecified reason */ + | 'FAILED' +/** + * An upstream endpoint's response to a TCP `keepalive` message from Cloudflare. + */ +declare type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus = + | 0 /** Unknown */ + | 1 /** no keepalives (not found) */ + | 2 /** no connection re-use, opening keepalive connection failed */ + | 3 /** no connection re-use, keepalive accepted and saved */ + | 4 /** connection re-use, refused by the origin server (`TCP FIN`) */ + | 5 /** connection re-use, accepted by the origin server */ +/** ISO 3166-1 Alpha-2 codes */ +declare type Iso3166Alpha2Code = + | 'AD' + | 'AE' + | 'AF' + | 'AG' + | 'AI' + | 'AL' + | 'AM' + | 'AO' + | 'AQ' + | 'AR' + | 'AS' + | 'AT' + | 'AU' + | 'AW' + | 'AX' + | 'AZ' + | 'BA' + | 'BB' + | 'BD' + | 'BE' + | 'BF' + | 'BG' + | 'BH' + | 'BI' + | 'BJ' + | 'BL' + | 'BM' + | 'BN' + | 'BO' + | 'BQ' + | 'BR' + | 'BS' + | 'BT' + | 'BV' + | 'BW' + | 'BY' + | 'BZ' + | 'CA' + | 'CC' + | 'CD' + | 'CF' + | 'CG' + | 'CH' + | 'CI' + | 'CK' + | 'CL' + | 'CM' + | 'CN' + | 'CO' + | 'CR' + | 'CU' + | 'CV' + | 'CW' + | 'CX' + | 'CY' + | 'CZ' + | 'DE' + | 'DJ' + | 'DK' + | 'DM' + | 'DO' + | 'DZ' + | 'EC' + | 'EE' + | 'EG' + | 'EH' + | 'ER' + | 'ES' + | 'ET' + | 'FI' + | 'FJ' + | 'FK' + | 'FM' + | 'FO' + | 'FR' + | 'GA' + | 'GB' + | 'GD' + | 'GE' + | 'GF' + | 'GG' + | 'GH' + | 'GI' + | 'GL' + | 'GM' + | 'GN' + | 'GP' + | 'GQ' + | 'GR' + | 'GS' + | 'GT' + | 'GU' + | 'GW' + | 'GY' + | 'HK' + | 'HM' + | 'HN' + | 'HR' + | 'HT' + | 'HU' + | 'ID' + | 'IE' + | 'IL' + | 'IM' + | 'IN' + | 'IO' + | 'IQ' + | 'IR' + | 'IS' + | 'IT' + | 'JE' + | 'JM' + | 'JO' + | 'JP' + | 'KE' + | 'KG' + | 'KH' + | 'KI' + | 'KM' + | 'KN' + | 'KP' + | 'KR' + | 'KW' + | 'KY' + | 'KZ' + | 'LA' + | 'LB' + | 'LC' + | 'LI' + | 'LK' + | 'LR' + | 'LS' + | 'LT' + | 'LU' + | 'LV' + | 'LY' + | 'MA' + | 'MC' + | 'MD' + | 'ME' + | 'MF' + | 'MG' + | 'MH' + | 'MK' + | 'ML' + | 'MM' + | 'MN' + | 'MO' + | 'MP' + | 'MQ' + | 'MR' + | 'MS' + | 'MT' + | 'MU' + | 'MV' + | 'MW' + | 'MX' + | 'MY' + | 'MZ' + | 'NA' + | 'NC' + | 'NE' + | 'NF' + | 'NG' + | 'NI' + | 'NL' + | 'NO' + | 'NP' + | 'NR' + | 'NU' + | 'NZ' + | 'OM' + | 'PA' + | 'PE' + | 'PF' + | 'PG' + | 'PH' + | 'PK' + | 'PL' + | 'PM' + | 'PN' + | 'PR' + | 'PS' + | 'PT' + | 'PW' + | 'PY' + | 'QA' + | 'RE' + | 'RO' + | 'RS' + | 'RU' + | 'RW' + | 'SA' + | 'SB' + | 'SC' + | 'SD' + | 'SE' + | 'SG' + | 'SH' + | 'SI' + | 'SJ' + | 'SK' + | 'SL' + | 'SM' + | 'SN' + | 'SO' + | 'SR' + | 'SS' + | 'ST' + | 'SV' + | 'SX' + | 'SY' + | 'SZ' + | 'TC' + | 'TD' + | 'TF' + | 'TG' + | 'TH' + | 'TJ' + | 'TK' + | 'TL' + | 'TM' + | 'TN' + | 'TO' + | 'TR' + | 'TT' + | 'TV' + | 'TW' + | 'TZ' + | 'UA' + | 'UG' + | 'UM' + | 'US' + | 'UY' + | 'UZ' + | 'VA' + | 'VC' + | 'VE' + | 'VG' + | 'VI' + | 'VN' + | 'VU' + | 'WF' + | 'WS' + | 'YE' + | 'YT' + | 'ZA' + | 'ZM' + | 'ZW' +/** The 2-letter continent codes Cloudflare uses */ +declare type ContinentCode = 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA' +type CfProperties = + | IncomingRequestCfProperties + | RequestInitCfProperties +interface D1Meta { + duration: number + size_after: number + rows_read: number + rows_written: number + last_row_id: number + changed_db: boolean + changes: number + /** + * The region of the database instance that executed the query. + */ + served_by_region?: string + /** + * True if-and-only-if the database instance that executed the query was the primary. + */ + served_by_primary?: boolean + timings?: { + /** + * The duration of the SQL query execution by the database instance. It doesn't include any network time. + */ + sql_duration_ms: number + } + /** + * Number of total attempts to execute the query, due to automatic retries. + * Note: All other fields in the response like `timings` only apply to the last attempt. + */ + total_attempts?: number +} +interface D1Response { + success: true + meta: D1Meta & Record + error?: never +} +type D1Result = D1Response & { + results: T[] +} +interface D1ExecResult { + count: number + duration: number +} +type D1SessionConstraint = + // Indicates that the first query should go to the primary, and the rest queries + // using the same D1DatabaseSession will go to any replica that is consistent with + // the bookmark maintained by the session (returned by the first query). + | 'first-primary' + // Indicates that the first query can go anywhere (primary or replica), and the rest queries + // using the same D1DatabaseSession will go to any replica that is consistent with + // the bookmark maintained by the session (returned by the first query). + | 'first-unconstrained' +type D1SessionBookmark = string +declare abstract class D1Database { + prepare(query: string): D1PreparedStatement + batch(statements: D1PreparedStatement[]): Promise[]> + exec(query: string): Promise + /** + * Creates a new D1 Session anchored at the given constraint or the bookmark. + * All queries executed using the created session will have sequential consistency, + * meaning that all writes done through the session will be visible in subsequent reads. + * + * @param constraintOrBookmark Either the session constraint or the explicit bookmark to anchor the created session. + */ + withSession(constraintOrBookmark?: D1SessionBookmark | D1SessionConstraint): D1DatabaseSession + /** + * @deprecated dump() will be removed soon, only applies to deprecated alpha v1 databases. + */ + dump(): Promise +} +declare abstract class D1DatabaseSession { + prepare(query: string): D1PreparedStatement + batch(statements: D1PreparedStatement[]): Promise[]> + /** + * @returns The latest session bookmark across all executed queries on the session. + * If no query has been executed yet, `null` is returned. + */ + getBookmark(): D1SessionBookmark | null +} +declare abstract class D1PreparedStatement { + bind(...values: unknown[]): D1PreparedStatement + first(colName: string): Promise + first>(): Promise + run>(): Promise> + all>(): Promise> + raw(options: { columnNames: true }): Promise<[string[], ...T[]]> + raw(options?: { columnNames?: false }): Promise +} +// `Disposable` was added to TypeScript's standard lib types in version 5.2. +// To support older TypeScript versions, define an empty `Disposable` interface. +// Users won't be able to use `using`/`Symbol.dispose` without upgrading to 5.2, +// but this will ensure type checking on older versions still passes. +// TypeScript's interface merging will ensure our empty interface is effectively +// ignored when `Disposable` is included in the standard lib. +interface Disposable {} +/** + * An email message that can be sent from a Worker. + */ +interface EmailMessage { + /** + * Envelope From attribute of the email message. + */ + readonly from: string + /** + * Envelope To attribute of the email message. + */ + readonly to: string +} +/** + * An email message that is sent to a consumer Worker and can be rejected/forwarded. + */ +interface ForwardableEmailMessage extends EmailMessage { + /** + * Stream of the email message content. + */ + readonly raw: ReadableStream + /** + * An [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers). + */ + readonly headers: Headers + /** + * Size of the email message content. + */ + readonly rawSize: number + /** + * Reject this email message by returning a permanent SMTP error back to the connecting client including the given reason. + * @param reason The reject reason. + * @returns void + */ + setReject(reason: string): void + /** + * Forward this email message to a verified destination address of the account. + * @param rcptTo Verified destination address. + * @param headers A [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers). + * @returns A promise that resolves when the email message is forwarded. + */ + forward(rcptTo: string, headers?: Headers): Promise + /** + * Reply to the sender of this email message with a new EmailMessage object. + * @param message The reply message. + * @returns A promise that resolves when the email message is replied. + */ + reply(message: EmailMessage): Promise +} +/** + * A binding that allows a Worker to send email messages. + */ +interface SendEmail { + send(message: EmailMessage): Promise +} +declare abstract class EmailEvent extends ExtendableEvent { + readonly message: ForwardableEmailMessage +} +declare type EmailExportedHandler = ( + message: ForwardableEmailMessage, + env: Env, + ctx: ExecutionContext +) => void | Promise +declare module 'cloudflare:email' { + let _EmailMessage: { + prototype: EmailMessage + new (from: string, to: string, raw: ReadableStream | string): EmailMessage + } + export { _EmailMessage as EmailMessage } +} +/** + * Hello World binding to serve as an explanatory example. DO NOT USE + */ +interface HelloWorldBinding { + /** + * Retrieve the current stored value + */ + get(): Promise<{ + value: string + ms?: number + }> + /** + * Set a new stored value + */ + set(value: string): Promise +} +interface Hyperdrive { + /** + * Connect directly to Hyperdrive as if it's your database, returning a TCP socket. + * + * Calling this method returns an idential socket to if you call + * `connect("host:port")` using the `host` and `port` fields from this object. + * Pick whichever approach works better with your preferred DB client library. + * + * Note that this socket is not yet authenticated -- it's expected that your + * code (or preferably, the client library of your choice) will authenticate + * using the information in this class's readonly fields. + */ + connect(): Socket + /** + * A valid DB connection string that can be passed straight into the typical + * client library/driver/ORM. This will typically be the easiest way to use + * Hyperdrive. + */ + readonly connectionString: string + /* + * A randomly generated hostname that is only valid within the context of the + * currently running Worker which, when passed into `connect()` function from + * the "cloudflare:sockets" module, will connect to the Hyperdrive instance + * for your database. + */ + readonly host: string + /* + * The port that must be paired the the host field when connecting. + */ + readonly port: number + /* + * The username to use when authenticating to your database via Hyperdrive. + * Unlike the host and password, this will be the same every time + */ + readonly user: string + /* + * The randomly generated password to use when authenticating to your + * database via Hyperdrive. Like the host field, this password is only valid + * within the context of the currently running Worker instance from which + * it's read. + */ + readonly password: string + /* + * The name of the database to connect to. + */ + readonly database: string +} +// Copyright (c) 2024 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +type ImageInfoResponse = + | { + format: 'image/svg+xml' + } + | { + format: string + fileSize: number + width: number + height: number + } +type ImageTransform = { + width?: number + height?: number + background?: string + blur?: number + border?: + | { + color?: string + width?: number + } + | { + top?: number + bottom?: number + left?: number + right?: number + } + brightness?: number + contrast?: number + fit?: 'scale-down' | 'contain' | 'pad' | 'squeeze' | 'cover' | 'crop' + flip?: 'h' | 'v' | 'hv' + gamma?: number + segment?: 'foreground' + gravity?: + | 'face' + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'center' + | 'auto' + | 'entropy' + | { + x?: number + y?: number + mode: 'remainder' | 'box-center' + } + rotate?: 0 | 90 | 180 | 270 + saturation?: number + sharpen?: number + trim?: + | 'border' + | { + top?: number + bottom?: number + left?: number + right?: number + width?: number + height?: number + border?: + | boolean + | { + color?: string + tolerance?: number + keep?: number + } + } +} +type ImageDrawOptions = { + opacity?: number + repeat?: boolean | string + top?: number + left?: number + bottom?: number + right?: number +} +type ImageInputOptions = { + encoding?: 'base64' +} +type ImageOutputOptions = { + format: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/avif' | 'rgb' | 'rgba' + quality?: number + background?: string + anim?: boolean +} +interface ImagesBinding { + /** + * Get image metadata (type, width and height) + * @throws {@link ImagesError} with code 9412 if input is not an image + * @param stream The image bytes + */ + info(stream: ReadableStream, options?: ImageInputOptions): Promise + /** + * Begin applying a series of transformations to an image + * @param stream The image bytes + * @returns A transform handle + */ + input(stream: ReadableStream, options?: ImageInputOptions): ImageTransformer +} +interface ImageTransformer { + /** + * Apply transform next, returning a transform handle. + * You can then apply more transformations, draw, or retrieve the output. + * @param transform + */ + transform(transform: ImageTransform): ImageTransformer + /** + * Draw an image on this transformer, returning a transform handle. + * You can then apply more transformations, draw, or retrieve the output. + * @param image The image (or transformer that will give the image) to draw + * @param options The options configuring how to draw the image + */ + draw( + image: ReadableStream | ImageTransformer, + options?: ImageDrawOptions + ): ImageTransformer + /** + * Retrieve the image that results from applying the transforms to the + * provided input + * @param options Options that apply to the output e.g. output format + */ + output(options: ImageOutputOptions): Promise +} +type ImageTransformationOutputOptions = { + encoding?: 'base64' +} +interface ImageTransformationResult { + /** + * The image as a response, ready to store in cache or return to users + */ + response(): Response + /** + * The content type of the returned image + */ + contentType(): string + /** + * The bytes of the response + */ + image(options?: ImageTransformationOutputOptions): ReadableStream +} +interface ImagesError extends Error { + readonly code: number + readonly message: string + readonly stack?: string +} +/** + * Media binding for transforming media streams. + * Provides the entry point for media transformation operations. + */ +interface MediaBinding { + /** + * Creates a media transformer from an input stream. + * @param media - The input media bytes + * @returns A MediaTransformer instance for applying transformations + */ + input(media: ReadableStream): MediaTransformer +} +/** + * Media transformer for applying transformation operations to media content. + * Handles sizing, fitting, and other input transformation parameters. + */ +interface MediaTransformer { + /** + * Applies transformation options to the media content. + * @param transform - Configuration for how the media should be transformed + * @returns A generator for producing the transformed media output + */ + transform(transform: MediaTransformationInputOptions): MediaTransformationGenerator +} +/** + * Generator for producing media transformation results. + * Configures the output format and parameters for the transformed media. + */ +interface MediaTransformationGenerator { + /** + * Generates the final media output with specified options. + * @param output - Configuration for the output format and parameters + * @returns The final transformation result containing the transformed media + */ + output(output: MediaTransformationOutputOptions): MediaTransformationResult +} +/** + * Result of a media transformation operation. + * Provides multiple ways to access the transformed media content. + */ +interface MediaTransformationResult { + /** + * Returns the transformed media as a readable stream of bytes. + * @returns A stream containing the transformed media data + */ + media(): ReadableStream + /** + * Returns the transformed media as an HTTP response object. + * @returns The transformed media as a Response, ready to store in cache or return to users + */ + response(): Response + /** + * Returns the MIME type of the transformed media. + * @returns The content type string (e.g., 'image/jpeg', 'video/mp4') + */ + contentType(): string +} +/** + * Configuration options for transforming media input. + * Controls how the media should be resized and fitted. + */ +type MediaTransformationInputOptions = { + /** How the media should be resized to fit the specified dimensions */ + fit?: 'contain' | 'cover' | 'scale-down' + /** Target width in pixels */ + width?: number + /** Target height in pixels */ + height?: number +} +/** + * Configuration options for Media Transformations output. + * Controls the format, timing, and type of the generated output. + */ +type MediaTransformationOutputOptions = { + /** + * Output mode determining the type of media to generate + */ + mode?: 'video' | 'spritesheet' | 'frame' | 'audio' + /** Whether to include audio in the output */ + audio?: boolean + /** + * Starting timestamp for frame extraction or start time for clips. (e.g. '2s'). + */ + time?: string + /** + * Duration for video clips, audio extraction, and spritesheet generation (e.g. '5s'). + */ + duration?: string + /** + * Output format for the generated media. + */ + format?: 'jpg' | 'png' | 'm4a' +} +/** + * Error object for media transformation operations. + * Extends the standard Error interface with additional media-specific information. + */ +interface MediaError extends Error { + readonly code: number + readonly message: string + readonly stack?: string +} +type Params

= Record +type EventContext = { + request: Request> + functionPath: string + waitUntil: (promise: Promise) => void + passThroughOnException: () => void + next: (input?: Request | string, init?: RequestInit) => Promise + env: Env & { + ASSETS: { + fetch: typeof fetch + } + } + params: Params

+ data: Data +} +type PagesFunction< + Env = unknown, + Params extends string = any, + Data extends Record = Record, +> = (context: EventContext) => Response | Promise +type EventPluginContext = { + request: Request> + functionPath: string + waitUntil: (promise: Promise) => void + passThroughOnException: () => void + next: (input?: Request | string, init?: RequestInit) => Promise + env: Env & { + ASSETS: { + fetch: typeof fetch + } + } + params: Params

+ data: Data + pluginArgs: PluginArgs +} +type PagesPluginFunction< + Env = unknown, + Params extends string = any, + Data extends Record = Record, + PluginArgs = unknown, +> = (context: EventPluginContext) => Response | Promise +declare module 'assets:*' { + export const onRequest: PagesFunction +} +// Copyright (c) 2022-2023 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +declare module 'cloudflare:pipelines' { + export abstract class PipelineTransformationEntrypoint< + Env = unknown, + I extends PipelineRecord = PipelineRecord, + O extends PipelineRecord = PipelineRecord, + > { + protected env: Env + protected ctx: ExecutionContext + constructor(ctx: ExecutionContext, env: Env) + /** + * run recieves an array of PipelineRecord which can be + * transformed and returned to the pipeline + * @param records Incoming records from the pipeline to be transformed + * @param metadata Information about the specific pipeline calling the transformation entrypoint + * @returns A promise containing the transformed PipelineRecord array + */ + public run(records: I[], metadata: PipelineBatchMetadata): Promise + } + export type PipelineRecord = Record + export type PipelineBatchMetadata = { + pipelineId: string + pipelineName: string + } + export interface Pipeline { + /** + * The Pipeline interface represents the type of a binding to a Pipeline + * + * @param records The records to send to the pipeline + */ + send(records: T[]): Promise + } +} +// PubSubMessage represents an incoming PubSub message. +// The message includes metadata about the broker, the client, and the payload +// itself. +// https://developers.cloudflare.com/pub-sub/ +interface PubSubMessage { + // Message ID + readonly mid: number + // MQTT broker FQDN in the form mqtts://BROKER.NAMESPACE.cloudflarepubsub.com:PORT + readonly broker: string + // The MQTT topic the message was sent on. + readonly topic: string + // The client ID of the client that published this message. + readonly clientId: string + // The unique identifier (JWT ID) used by the client to authenticate, if token + // auth was used. + readonly jti?: string + // A Unix timestamp (seconds from Jan 1, 1970), set when the Pub/Sub Broker + // received the message from the client. + readonly receivedAt: number + // An (optional) string with the MIME type of the payload, if set by the + // client. + readonly contentType: string + // Set to 1 when the payload is a UTF-8 string + // https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901063 + readonly payloadFormatIndicator: number + // Pub/Sub (MQTT) payloads can be UTF-8 strings, or byte arrays. + // You can use payloadFormatIndicator to inspect this before decoding. + payload: string | Uint8Array +} +// JsonWebKey extended by kid parameter +interface JsonWebKeyWithKid extends JsonWebKey { + // Key Identifier of the JWK + readonly kid: string +} +interface RateLimitOptions { + key: string +} +interface RateLimitOutcome { + success: boolean +} +interface RateLimit { + /** + * Rate limit a request based on the provided options. + * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/ + * @returns A promise that resolves with the outcome of the rate limit. + */ + limit(options: RateLimitOptions): Promise +} +// Namespace for RPC utility types. Unfortunately, we can't use a `module` here as these types need +// to referenced by `Fetcher`. This is included in the "importable" version of the types which +// strips all `module` blocks. +declare namespace Rpc { + // Branded types for identifying `WorkerEntrypoint`/`DurableObject`/`Target`s. + // TypeScript uses *structural* typing meaning anything with the same shape as type `T` is a `T`. + // For the classes exported by `cloudflare:workers` we want *nominal* typing (i.e. we only want to + // accept `WorkerEntrypoint` from `cloudflare:workers`, not any other class with the same shape) + export const __RPC_STUB_BRAND: '__RPC_STUB_BRAND' + export const __RPC_TARGET_BRAND: '__RPC_TARGET_BRAND' + export const __WORKER_ENTRYPOINT_BRAND: '__WORKER_ENTRYPOINT_BRAND' + export const __DURABLE_OBJECT_BRAND: '__DURABLE_OBJECT_BRAND' + export const __WORKFLOW_ENTRYPOINT_BRAND: '__WORKFLOW_ENTRYPOINT_BRAND' + export interface RpcTargetBranded { + [__RPC_TARGET_BRAND]: never + } + export interface WorkerEntrypointBranded { + [__WORKER_ENTRYPOINT_BRAND]: never + } + export interface DurableObjectBranded { + [__DURABLE_OBJECT_BRAND]: never + } + export interface WorkflowEntrypointBranded { + [__WORKFLOW_ENTRYPOINT_BRAND]: never + } + export type EntrypointBranded = + | WorkerEntrypointBranded + | DurableObjectBranded + | WorkflowEntrypointBranded + // Types that can be used through `Stub`s + export type Stubable = RpcTargetBranded | ((...args: any[]) => any) + // Types that can be passed over RPC + // The reason for using a generic type here is to build a serializable subset of structured + // cloneable composite types. This allows types defined with the "interface" keyword to pass the + // serializable check as well. Otherwise, only types defined with the "type" keyword would pass. + type Serializable = + // Structured cloneables + | BaseType + // Structured cloneable composites + | Map< + T extends Map ? Serializable : never, + T extends Map ? Serializable : never + > + | Set ? Serializable : never> + | ReadonlyArray ? Serializable : never> + | { + [K in keyof T]: K extends number | string ? Serializable : never + } + // Special types + | Stub + // Serialized as stubs, see `Stubify` + | Stubable + // Base type for all RPC stubs, including common memory management methods. + // `T` is used as a marker type for unwrapping `Stub`s later. + interface StubBase extends Disposable { + [__RPC_STUB_BRAND]: T + dup(): this + } + export type Stub = Provider & StubBase + // This represents all the types that can be sent as-is over an RPC boundary + type BaseType = + | void + | undefined + | null + | boolean + | number + | bigint + | string + | TypedArray + | ArrayBuffer + | DataView + | Date + | Error + | RegExp + | ReadableStream + | WritableStream + | Request + | Response + | Headers + // Recursively rewrite all `Stubable` types with `Stub`s + // prettier-ignore + type Stubify = T extends Stubable ? Stub : T extends Map ? Map, Stubify> : T extends Set ? Set> : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends BaseType ? T : T extends { + [key: string | number]: any; + } ? { + [K in keyof T]: Stubify; + } : T; + // Recursively rewrite all `Stub`s with the corresponding `T`s. + // Note we use `StubBase` instead of `Stub` here to avoid circular dependencies: + // `Stub` depends on `Provider`, which depends on `Unstubify`, which would depend on `Stub`. + // prettier-ignore + type Unstubify = T extends StubBase ? V : T extends Map ? Map, Unstubify> : T extends Set ? Set> : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends BaseType ? T : T extends { + [key: string | number]: unknown; + } ? { + [K in keyof T]: Unstubify; + } : T; + type UnstubifyAll = { + [I in keyof A]: Unstubify + } + // Utility type for adding `Provider`/`Disposable`s to `object` types only. + // Note `unknown & T` is equivalent to `T`. + type MaybeProvider = T extends object ? Provider : unknown + type MaybeDisposable = T extends object ? Disposable : unknown + // Type for method return or property on an RPC interface. + // - Stubable types are replaced by stubs. + // - Serializable types are passed by value, with stubable types replaced by stubs + // and a top-level `Disposer`. + // Everything else can't be passed over PRC. + // Technically, we use custom thenables here, but they quack like `Promise`s. + // Intersecting with `(Maybe)Provider` allows pipelining. + // prettier-ignore + type Result = R extends Stubable ? Promise> & Provider : R extends Serializable ? Promise & MaybeDisposable> & MaybeProvider : never; + // Type for method or property on an RPC interface. + // For methods, unwrap `Stub`s in parameters, and rewrite returns to be `Result`s. + // Unwrapping `Stub`s allows calling with `Stubable` arguments. + // For properties, rewrite types to be `Result`s. + // In each case, unwrap `Promise`s. + type MethodOrProperty = V extends (...args: infer P) => infer R + ? (...args: UnstubifyAll

) => Result> + : Result> + // Type for the callable part of an `Provider` if `T` is callable. + // This is intersected with methods/properties. + type MaybeCallableProvider = T extends (...args: any[]) => any ? MethodOrProperty : unknown + // Base type for all other types providing RPC-like interfaces. + // Rewrites all methods/properties to be `MethodOrProperty`s, while preserving callable types. + // `Reserved` names (e.g. stub method names like `dup()`) and symbols can't be accessed over RPC. + export type Provider< + T extends object, + Reserved extends string = never, + > = MaybeCallableProvider & { + [K in Exclude>]: MethodOrProperty + } +} +declare namespace Cloudflare { + // Type of `env`. + // + // The specific project can extend `Env` by redeclaring it in project-specific files. Typescript + // will merge all declarations. + // + // You can use `wrangler types` to generate the `Env` type automatically. + interface Env {} + // Project-specific parameters used to inform types. + // + // This interface is, again, intended to be declared in project-specific files, and then that + // declaration will be merged with this one. + // + // A project should have a declaration like this: + // + // interface GlobalProps { + // // Declares the main module's exports. Used to populate Cloudflare.Exports aka the type + // // of `ctx.exports`. + // mainModule: typeof import("my-main-module"); + // + // // Declares which of the main module's exports are configured with durable storage, and + // // thus should behave as Durable Object namsepace bindings. + // durableNamespaces: "MyDurableObject" | "AnotherDurableObject"; + // } + // + // You can use `wrangler types` to generate `GlobalProps` automatically. + interface GlobalProps {} + // Evaluates to the type of a property in GlobalProps, defaulting to `Default` if it is not + // present. + type GlobalProp = K extends keyof GlobalProps + ? GlobalProps[K] + : Default + // The type of the program's main module exports, if known. Requires `GlobalProps` to declare the + // `mainModule` property. + type MainModule = GlobalProp<'mainModule', {}> + // The type of ctx.exports, which contains loopback bindings for all top-level exports. + type Exports = { + [K in keyof MainModule]: LoopbackForExport & + // If the export is listed in `durableNamespaces`, then it is also a + // DurableObjectNamespace. + (K extends GlobalProp<'durableNamespaces', never> + ? MainModule[K] extends new (...args: any[]) => infer DoInstance + ? DoInstance extends Rpc.DurableObjectBranded + ? DurableObjectNamespace + : DurableObjectNamespace + : DurableObjectNamespace + : {}) + } +} +declare module 'cloudflare:node' { + export interface DefaultHandler { + fetch?(request: Request): Response | Promise + tail?(events: TraceItem[]): void | Promise + trace?(traces: TraceItem[]): void | Promise + scheduled?(controller: ScheduledController): void | Promise + queue?(batch: MessageBatch): void | Promise + test?(controller: TestController): void | Promise + } + export function httpServerHandler( + options: { + port: number + }, + handlers?: Omit + ): DefaultHandler +} +declare namespace CloudflareWorkersModule { + export type RpcStub = Rpc.Stub + export const RpcStub: { + new (value: T): Rpc.Stub + } + export abstract class RpcTarget implements Rpc.RpcTargetBranded { + [Rpc.__RPC_TARGET_BRAND]: never + } + // `protected` fields don't appear in `keyof`s, so can't be accessed over RPC + export abstract class WorkerEntrypoint + implements Rpc.WorkerEntrypointBranded + { + [Rpc.__WORKER_ENTRYPOINT_BRAND]: never + protected ctx: ExecutionContext + protected env: Env + constructor(ctx: ExecutionContext, env: Env) + fetch?(request: Request): Response | Promise + tail?(events: TraceItem[]): void | Promise + trace?(traces: TraceItem[]): void | Promise + scheduled?(controller: ScheduledController): void | Promise + queue?(batch: MessageBatch): void | Promise + test?(controller: TestController): void | Promise + } + export abstract class DurableObject + implements Rpc.DurableObjectBranded + { + [Rpc.__DURABLE_OBJECT_BRAND]: never + protected ctx: DurableObjectState + protected env: Env + constructor(ctx: DurableObjectState, env: Env) + fetch?(request: Request): Response | Promise + alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise + webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise + webSocketClose?( + ws: WebSocket, + code: number, + reason: string, + wasClean: boolean + ): void | Promise + webSocketError?(ws: WebSocket, error: unknown): void | Promise + } + export type WorkflowDurationLabel = + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'month' + | 'year' + export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number + export type WorkflowDelayDuration = WorkflowSleepDuration + export type WorkflowTimeoutDuration = WorkflowSleepDuration + export type WorkflowRetentionDuration = WorkflowSleepDuration + export type WorkflowBackoff = 'constant' | 'linear' | 'exponential' + export type WorkflowStepConfig = { + retries?: { + limit: number + delay: WorkflowDelayDuration | number + backoff?: WorkflowBackoff + } + timeout?: WorkflowTimeoutDuration | number + } + export type WorkflowEvent = { + payload: Readonly + timestamp: Date + instanceId: string + } + export type WorkflowStepEvent = { + payload: Readonly + timestamp: Date + type: string + } + export abstract class WorkflowStep { + do>(name: string, callback: () => Promise): Promise + do>( + name: string, + config: WorkflowStepConfig, + callback: () => Promise + ): Promise + sleep: (name: string, duration: WorkflowSleepDuration) => Promise + sleepUntil: (name: string, timestamp: Date | number) => Promise + waitForEvent>( + name: string, + options: { + type: string + timeout?: WorkflowTimeoutDuration | number + } + ): Promise> + } + export abstract class WorkflowEntrypoint< + Env = unknown, + T extends Rpc.Serializable | unknown = unknown, + > + implements Rpc.WorkflowEntrypointBranded + { + [Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never + protected ctx: ExecutionContext + protected env: Env + constructor(ctx: ExecutionContext, env: Env) + run(event: Readonly>, step: WorkflowStep): Promise + } + export function waitUntil(promise: Promise): void + export const env: Cloudflare.Env +} +declare module 'cloudflare:workers' { + export = CloudflareWorkersModule +} +interface SecretsStoreSecret { + /** + * Get a secret from the Secrets Store, returning a string of the secret value + * if it exists, or throws an error if it does not exist + */ + get(): Promise +} +declare module 'cloudflare:sockets' { + function _connect(address: string | SocketAddress, options?: SocketOptions): Socket + export { _connect as connect } +} +declare namespace TailStream { + interface Header { + readonly name: string + readonly value: string + } + interface FetchEventInfo { + readonly type: 'fetch' + readonly method: string + readonly url: string + readonly cfJson?: object + readonly headers: Header[] + } + interface JsRpcEventInfo { + readonly type: 'jsrpc' + readonly methodName: string + } + interface ScheduledEventInfo { + readonly type: 'scheduled' + readonly scheduledTime: Date + readonly cron: string + } + interface AlarmEventInfo { + readonly type: 'alarm' + readonly scheduledTime: Date + } + interface QueueEventInfo { + readonly type: 'queue' + readonly queueName: string + readonly batchSize: number + } + interface EmailEventInfo { + readonly type: 'email' + readonly mailFrom: string + readonly rcptTo: string + readonly rawSize: number + } + interface TraceEventInfo { + readonly type: 'trace' + readonly traces: (string | null)[] + } + interface HibernatableWebSocketEventInfoMessage { + readonly type: 'message' + } + interface HibernatableWebSocketEventInfoError { + readonly type: 'error' + } + interface HibernatableWebSocketEventInfoClose { + readonly type: 'close' + readonly code: number + readonly wasClean: boolean + } + interface HibernatableWebSocketEventInfo { + readonly type: 'hibernatableWebSocket' + readonly info: + | HibernatableWebSocketEventInfoClose + | HibernatableWebSocketEventInfoError + | HibernatableWebSocketEventInfoMessage + } + interface CustomEventInfo { + readonly type: 'custom' + } + interface FetchResponseInfo { + readonly type: 'fetch' + readonly statusCode: number + } + type EventOutcome = + | 'ok' + | 'canceled' + | 'exception' + | 'unknown' + | 'killSwitch' + | 'daemonDown' + | 'exceededCpu' + | 'exceededMemory' + | 'loadShed' + | 'responseStreamDisconnected' + | 'scriptNotFound' + interface ScriptVersion { + readonly id: string + readonly tag?: string + readonly message?: string + } + interface Onset { + readonly type: 'onset' + readonly attributes: Attribute[] + // id for the span being opened by this Onset event. + readonly spanId: string + readonly dispatchNamespace?: string + readonly entrypoint?: string + readonly executionModel: string + readonly scriptName?: string + readonly scriptTags?: string[] + readonly scriptVersion?: ScriptVersion + readonly info: + | FetchEventInfo + | JsRpcEventInfo + | ScheduledEventInfo + | AlarmEventInfo + | QueueEventInfo + | EmailEventInfo + | TraceEventInfo + | HibernatableWebSocketEventInfo + | CustomEventInfo + } + interface Outcome { + readonly type: 'outcome' + readonly outcome: EventOutcome + readonly cpuTime: number + readonly wallTime: number + } + interface SpanOpen { + readonly type: 'spanOpen' + readonly name: string + // id for the span being opened by this SpanOpen event. + readonly spanId: string + readonly info?: FetchEventInfo | JsRpcEventInfo | Attributes + } + interface SpanClose { + readonly type: 'spanClose' + readonly outcome: EventOutcome + } + interface DiagnosticChannelEvent { + readonly type: 'diagnosticChannel' + readonly channel: string + readonly message: any + } + interface Exception { + readonly type: 'exception' + readonly name: string + readonly message: string + readonly stack?: string + } + interface Log { + readonly type: 'log' + readonly level: 'debug' | 'error' | 'info' | 'log' | 'warn' + readonly message: object + } + // This marks the worker handler return information. + // This is separate from Outcome because the worker invocation can live for a long time after + // returning. For example - Websockets that return an http upgrade response but then continue + // streaming information or SSE http connections. + interface Return { + readonly type: 'return' + readonly info?: FetchResponseInfo + } + interface Attribute { + readonly name: string + readonly value: string | string[] | boolean | boolean[] | number | number[] | bigint | bigint[] + } + interface Attributes { + readonly type: 'attributes' + readonly info: Attribute[] + } + type EventType = + | Onset + | Outcome + | SpanOpen + | SpanClose + | DiagnosticChannelEvent + | Exception + | Log + | Return + | Attributes + // Context in which this trace event lives. + interface SpanContext { + // Single id for the entire top-level invocation + // This should be a new traceId for the first worker stage invoked in the eyeball request and then + // same-account service-bindings should reuse the same traceId but cross-account service-bindings + // should use a new traceId. + readonly traceId: string + // spanId in which this event is handled + // for Onset and SpanOpen events this would be the parent span id + // for Outcome and SpanClose these this would be the span id of the opening Onset and SpanOpen events + // For Hibernate and Mark this would be the span under which they were emitted. + // spanId is not set ONLY if: + // 1. This is an Onset event + // 2. We are not inherting any SpanContext. (e.g. this is a cross-account service binding or a new top-level invocation) + readonly spanId?: string + } + interface TailEvent { + // invocation id of the currently invoked worker stage. + // invocation id will always be unique to every Onset event and will be the same until the Outcome event. + readonly invocationId: string + // Inherited spanContext for this event. + readonly spanContext: SpanContext + readonly timestamp: Date + readonly sequence: number + readonly event: Event + } + type TailEventHandler = ( + event: TailEvent + ) => void | Promise + type TailEventHandlerObject = { + outcome?: TailEventHandler + spanOpen?: TailEventHandler + spanClose?: TailEventHandler + diagnosticChannel?: TailEventHandler + exception?: TailEventHandler + log?: TailEventHandler + return?: TailEventHandler + attributes?: TailEventHandler + } + type TailEventHandlerType = TailEventHandler | TailEventHandlerObject +} +// Copyright (c) 2022-2023 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +/** + * Data types supported for holding vector metadata. + */ +type VectorizeVectorMetadataValue = string | number | boolean | string[] +/** + * Additional information to associate with a vector. + */ +type VectorizeVectorMetadata = + | VectorizeVectorMetadataValue + | Record +type VectorFloatArray = Float32Array | Float64Array +interface VectorizeError { + code?: number + error: string +} +/** + * Comparison logic/operation to use for metadata filtering. + * + * This list is expected to grow as support for more operations are released. + */ +type VectorizeVectorMetadataFilterOp = '$eq' | '$ne' +/** + * Filter criteria for vector metadata used to limit the retrieved query result set. + */ +type VectorizeVectorMetadataFilter = { + [field: string]: + | Exclude + | null + | { + [Op in VectorizeVectorMetadataFilterOp]?: Exclude< + VectorizeVectorMetadataValue, + string[] + > | null + } +} +/** + * Supported distance metrics for an index. + * Distance metrics determine how other "similar" vectors are determined. + */ +type VectorizeDistanceMetric = 'euclidean' | 'cosine' | 'dot-product' +/** + * Metadata return levels for a Vectorize query. + * + * Default to "none". + * + * @property all Full metadata for the vector return set, including all fields (including those un-indexed) without truncation. This is a more expensive retrieval, as it requires additional fetching & reading of un-indexed data. + * @property indexed Return all metadata fields configured for indexing in the vector return set. This level of retrieval is "free" in that no additional overhead is incurred returning this data. However, note that indexed metadata is subject to truncation (especially for larger strings). + * @property none No indexed metadata will be returned. + */ +type VectorizeMetadataRetrievalLevel = 'all' | 'indexed' | 'none' +interface VectorizeQueryOptions { + topK?: number + namespace?: string + returnValues?: boolean + returnMetadata?: boolean | VectorizeMetadataRetrievalLevel + filter?: VectorizeVectorMetadataFilter +} +/** + * Information about the configuration of an index. + */ +type VectorizeIndexConfig = + | { + dimensions: number + metric: VectorizeDistanceMetric + } + | { + preset: string // keep this generic, as we'll be adding more presets in the future and this is only in a read capacity + } +/** + * Metadata about an existing index. + * + * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released. + * See {@link VectorizeIndexInfo} for its post-beta equivalent. + */ +interface VectorizeIndexDetails { + /** The unique ID of the index */ + readonly id: string + /** The name of the index. */ + name: string + /** (optional) A human readable description for the index. */ + description?: string + /** The index configuration, including the dimension size and distance metric. */ + config: VectorizeIndexConfig + /** The number of records containing vectors within the index. */ + vectorsCount: number +} +/** + * Metadata about an existing index. + */ +interface VectorizeIndexInfo { + /** The number of records containing vectors within the index. */ + vectorCount: number + /** Number of dimensions the index has been configured for. */ + dimensions: number + /** ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state. */ + processedUpToDatetime: number + /** UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state. */ + processedUpToMutation: number +} +/** + * Represents a single vector value set along with its associated metadata. + */ +interface VectorizeVector { + /** The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents. */ + id: string + /** The vector values */ + values: VectorFloatArray | number[] + /** The namespace this vector belongs to. */ + namespace?: string + /** Metadata associated with the vector. Includes the values of other fields and potentially additional details. */ + metadata?: Record +} +/** + * Represents a matched vector for a query along with its score and (if specified) the matching vector information. + */ +type VectorizeMatch = Pick, 'values'> & + Omit & { + /** The score or rank for similarity, when returned as a result */ + score: number + } +/** + * A set of matching {@link VectorizeMatch} for a particular query. + */ +interface VectorizeMatches { + matches: VectorizeMatch[] + count: number +} +/** + * Results of an operation that performed a mutation on a set of vectors. + * Here, `ids` is a list of vectors that were successfully processed. + * + * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released. + * See {@link VectorizeAsyncMutation} for its post-beta equivalent. + */ +interface VectorizeVectorMutation { + /* List of ids of vectors that were successfully processed. */ + ids: string[] + /* Total count of the number of processed vectors. */ + count: number +} +/** + * Result type indicating a mutation on the Vectorize Index. + * Actual mutations are processed async where the `mutationId` is the unique identifier for the operation. + */ +interface VectorizeAsyncMutation { + /** The unique identifier for the async mutation operation containing the changeset. */ + mutationId: string +} +/** + * A Vectorize Vector Search Index for querying vectors/embeddings. + * + * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released. + * See {@link Vectorize} for its new implementation. + */ +declare abstract class VectorizeIndex { + /** + * Get information about the currently bound index. + * @returns A promise that resolves with information about the current index. + */ + public describe(): Promise + /** + * Use the provided vector to perform a similarity search across the index. + * @param vector Input vector that will be used to drive the similarity search. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public query( + vector: VectorFloatArray | number[], + options?: VectorizeQueryOptions + ): Promise + /** + * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown. + * @param vectors List of vectors that will be inserted. + * @returns A promise that resolves with the ids & count of records that were successfully processed. + */ + public insert(vectors: VectorizeVector[]): Promise + /** + * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values. + * @param vectors List of vectors that will be upserted. + * @returns A promise that resolves with the ids & count of records that were successfully processed. + */ + public upsert(vectors: VectorizeVector[]): Promise + /** + * Delete a list of vectors with a matching id. + * @param ids List of vector ids that should be deleted. + * @returns A promise that resolves with the ids & count of records that were successfully processed (and thus deleted). + */ + public deleteByIds(ids: string[]): Promise + /** + * Get a list of vectors with a matching id. + * @param ids List of vector ids that should be returned. + * @returns A promise that resolves with the raw unscored vectors matching the id set. + */ + public getByIds(ids: string[]): Promise +} +/** + * A Vectorize Vector Search Index for querying vectors/embeddings. + * + * Mutations in this version are async, returning a mutation id. + */ +declare abstract class Vectorize { + /** + * Get information about the currently bound index. + * @returns A promise that resolves with information about the current index. + */ + public describe(): Promise + /** + * Use the provided vector to perform a similarity search across the index. + * @param vector Input vector that will be used to drive the similarity search. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public query( + vector: VectorFloatArray | number[], + options?: VectorizeQueryOptions + ): Promise + /** + * Use the provided vector-id to perform a similarity search across the index. + * @param vectorId Id for a vector in the index against which the index should be queried. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public queryById(vectorId: string, options?: VectorizeQueryOptions): Promise + /** + * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown. + * @param vectors List of vectors that will be inserted. + * @returns A promise that resolves with a unique identifier of a mutation containing the insert changeset. + */ + public insert(vectors: VectorizeVector[]): Promise + /** + * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values. + * @param vectors List of vectors that will be upserted. + * @returns A promise that resolves with a unique identifier of a mutation containing the upsert changeset. + */ + public upsert(vectors: VectorizeVector[]): Promise + /** + * Delete a list of vectors with a matching id. + * @param ids List of vector ids that should be deleted. + * @returns A promise that resolves with a unique identifier of a mutation containing the delete changeset. + */ + public deleteByIds(ids: string[]): Promise + /** + * Get a list of vectors with a matching id. + * @param ids List of vector ids that should be returned. + * @returns A promise that resolves with the raw unscored vectors matching the id set. + */ + public getByIds(ids: string[]): Promise +} +/** + * The interface for "version_metadata" binding + * providing metadata about the Worker Version using this binding. + */ +type WorkerVersionMetadata = { + /** The ID of the Worker Version using this binding */ + id: string + /** The tag of the Worker Version using this binding */ + tag: string + /** The timestamp of when the Worker Version was uploaded */ + timestamp: string +} +interface DynamicDispatchLimits { + /** + * Limit CPU time in milliseconds. + */ + cpuMs?: number + /** + * Limit number of subrequests. + */ + subRequests?: number +} +interface DynamicDispatchOptions { + /** + * Limit resources of invoked Worker script. + */ + limits?: DynamicDispatchLimits + /** + * Arguments for outbound Worker script, if configured. + */ + outbound?: { + [key: string]: any + } +} +interface DispatchNamespace { + /** + * @param name Name of the Worker script. + * @param args Arguments to Worker script. + * @param options Options for Dynamic Dispatch invocation. + * @returns A Fetcher object that allows you to send requests to the Worker script. + * @throws If the Worker script does not exist in this dispatch namespace, an error will be thrown. + */ + get( + name: string, + args?: { + [key: string]: any + }, + options?: DynamicDispatchOptions + ): Fetcher +} +declare module 'cloudflare:workflows' { + /** + * NonRetryableError allows for a user to throw a fatal error + * that makes a Workflow instance fail immediately without triggering a retry + */ + export class NonRetryableError extends Error { + public constructor(message: string, name?: string) + } +} +declare abstract class Workflow { + /** + * Get a handle to an existing instance of the Workflow. + * @param id Id for the instance of this Workflow + * @returns A promise that resolves with a handle for the Instance + */ + public get(id: string): Promise + /** + * Create a new instance and return a handle to it. If a provided id exists, an error will be thrown. + * @param options Options when creating an instance including id and params + * @returns A promise that resolves with a handle for the Instance + */ + public create(options?: WorkflowInstanceCreateOptions): Promise + /** + * Create a batch of instances and return handle for all of them. If a provided id exists, an error will be thrown. + * `createBatch` is limited at 100 instances at a time or when the RPC limit for the batch (1MiB) is reached. + * @param batch List of Options when creating an instance including name and params + * @returns A promise that resolves with a list of handles for the created instances. + */ + public createBatch(batch: WorkflowInstanceCreateOptions[]): Promise +} +type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' +type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number +type WorkflowRetentionDuration = WorkflowSleepDuration +interface WorkflowInstanceCreateOptions { + /** + * An id for your Workflow instance. Must be unique within the Workflow. + */ + id?: string + /** + * The event payload the Workflow instance is triggered with + */ + params?: PARAMS + /** + * The retention policy for Workflow instance. + * Defaults to the maximum retention period available for the owner's account. + */ + retention?: { + successRetention?: WorkflowRetentionDuration + errorRetention?: WorkflowRetentionDuration + } +} +type InstanceStatus = { + status: + | 'queued' // means that instance is waiting to be started (see concurrency limits) + | 'running' + | 'paused' + | 'errored' + | 'terminated' // user terminated the instance while it was running + | 'complete' + | 'waiting' // instance is hibernating and waiting for sleep or event to finish + | 'waitingForPause' // instance is finishing the current work to pause + | 'unknown' + error?: string + output?: object +} +interface WorkflowError { + code?: number + message: string +} +declare abstract class WorkflowInstance { + public id: string + /** + * Pause the instance. + */ + public pause(): Promise + /** + * Resume the instance. If it is already running, an error will be thrown. + */ + public resume(): Promise + /** + * Terminate the instance. If it is errored, terminated or complete, an error will be thrown. + */ + public terminate(): Promise + /** + * Restart the instance. + */ + public restart(): Promise + /** + * Returns the current status of the instance. + */ + public status(): Promise + /** + * Send an event to this instance. + */ + public sendEvent({ type, payload }: { type: string; payload: unknown }): Promise +} diff --git a/templates/sync-cloudflare/worker-configuration.d.ts b/templates/sync-cloudflare/worker-configuration.d.ts new file mode 100644 index 0000000..c2321c4 --- /dev/null +++ b/templates/sync-cloudflare/worker-configuration.d.ts @@ -0,0 +1,8202 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types` (hash: d45bc36a51ae49551e7e1fdffb2538b0) +// Runtime types generated with workerd@1.20250617.0 2025-05-08 nodejs_compat +declare namespace Cloudflare { + interface Env { + TLDRAW_DURABLE_OBJECT: DurableObjectNamespace + TLDRAW_BUCKET: R2Bucket + } +} +interface Env extends Cloudflare.Env {} + +// Begin runtime types +/*! ***************************************************************************** +Copyright (c) Cloudflare. All rights reserved. +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* eslint-disable */ +// noinspection JSUnusedGlobalSymbols +declare var onmessage: never +/** + * An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) + */ +declare class DOMException extends Error { + constructor(message?: string, name?: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) */ + readonly message: string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) */ + readonly name: string + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) + */ + readonly code: number + static readonly INDEX_SIZE_ERR: number + static readonly DOMSTRING_SIZE_ERR: number + static readonly HIERARCHY_REQUEST_ERR: number + static readonly WRONG_DOCUMENT_ERR: number + static readonly INVALID_CHARACTER_ERR: number + static readonly NO_DATA_ALLOWED_ERR: number + static readonly NO_MODIFICATION_ALLOWED_ERR: number + static readonly NOT_FOUND_ERR: number + static readonly NOT_SUPPORTED_ERR: number + static readonly INUSE_ATTRIBUTE_ERR: number + static readonly INVALID_STATE_ERR: number + static readonly SYNTAX_ERR: number + static readonly INVALID_MODIFICATION_ERR: number + static readonly NAMESPACE_ERR: number + static readonly INVALID_ACCESS_ERR: number + static readonly VALIDATION_ERR: number + static readonly TYPE_MISMATCH_ERR: number + static readonly SECURITY_ERR: number + static readonly NETWORK_ERR: number + static readonly ABORT_ERR: number + static readonly URL_MISMATCH_ERR: number + static readonly QUOTA_EXCEEDED_ERR: number + static readonly TIMEOUT_ERR: number + static readonly INVALID_NODE_TYPE_ERR: number + static readonly DATA_CLONE_ERR: number + get stack(): any + set stack(value: any) +} +type WorkerGlobalScopeEventMap = { + fetch: FetchEvent + scheduled: ScheduledEvent + queue: QueueEvent + unhandledrejection: PromiseRejectionEvent + rejectionhandled: PromiseRejectionEvent +} +declare abstract class WorkerGlobalScope extends EventTarget { + EventTarget: typeof EventTarget +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */ +interface Console { + 'assert'(condition?: boolean, ...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */ + clear(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */ + count(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) */ + countReset(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) */ + debug(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) */ + dir(item?: any, options?: any): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) */ + dirxml(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) */ + error(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) */ + group(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) */ + groupCollapsed(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) */ + groupEnd(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) */ + info(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) */ + log(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) */ + table(tabularData?: any, properties?: string[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) */ + time(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) */ + timeEnd(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) */ + timeLog(label?: string, ...data: any[]): void + timeStamp(label?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) */ + trace(...data: any[]): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) */ + warn(...data: any[]): void +} +declare const console: Console +type BufferSource = ArrayBufferView | ArrayBuffer +type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array +declare namespace WebAssembly { + class CompileError extends Error { + constructor(message?: string) + } + class RuntimeError extends Error { + constructor(message?: string) + } + type ValueType = 'anyfunc' | 'externref' | 'f32' | 'f64' | 'i32' | 'i64' | 'v128' + interface GlobalDescriptor { + value: ValueType + mutable?: boolean + } + class Global { + constructor(descriptor: GlobalDescriptor, value?: any) + value: any + valueOf(): any + } + type ImportValue = ExportValue | number + type ModuleImports = Record + type Imports = Record + type ExportValue = Function | Global | Memory | Table + type Exports = Record + class Instance { + constructor(module: Module, imports?: Imports) + readonly exports: Exports + } + interface MemoryDescriptor { + initial: number + maximum?: number + shared?: boolean + } + class Memory { + constructor(descriptor: MemoryDescriptor) + readonly buffer: ArrayBuffer + grow(delta: number): number + } + type ImportExportKind = 'function' | 'global' | 'memory' | 'table' + interface ModuleExportDescriptor { + kind: ImportExportKind + name: string + } + interface ModuleImportDescriptor { + kind: ImportExportKind + module: string + name: string + } + abstract class Module { + static customSections(module: Module, sectionName: string): ArrayBuffer[] + static exports(module: Module): ModuleExportDescriptor[] + static imports(module: Module): ModuleImportDescriptor[] + } + type TableKind = 'anyfunc' | 'externref' + interface TableDescriptor { + element: TableKind + initial: number + maximum?: number + } + class Table { + constructor(descriptor: TableDescriptor, value?: any) + readonly length: number + get(index: number): any + grow(delta: number, value?: any): number + set(index: number, value?: any): void + } + function instantiate(module: Module, imports?: Imports): Promise + function validate(bytes: BufferSource): boolean +} +/** + * This ServiceWorker API interface represents the global execution context of a service worker. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope) + */ +interface ServiceWorkerGlobalScope extends WorkerGlobalScope { + DOMException: typeof DOMException + WorkerGlobalScope: typeof WorkerGlobalScope + btoa(data: string): string + atob(data: string): string + setTimeout(callback: (...args: any[]) => void, msDelay?: number): number + setTimeout( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args + ): number + clearTimeout(timeoutId: number | null): void + setInterval(callback: (...args: any[]) => void, msDelay?: number): number + setInterval( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args + ): number + clearInterval(timeoutId: number | null): void + queueMicrotask(task: Function): void + structuredClone(value: T, options?: StructuredSerializeOptions): T + reportError(error: any): void + fetch(input: RequestInfo | URL, init?: RequestInit): Promise + self: ServiceWorkerGlobalScope + crypto: Crypto + caches: CacheStorage + scheduler: Scheduler + performance: Performance + Cloudflare: Cloudflare + readonly origin: string + Event: typeof Event + ExtendableEvent: typeof ExtendableEvent + CustomEvent: typeof CustomEvent + PromiseRejectionEvent: typeof PromiseRejectionEvent + FetchEvent: typeof FetchEvent + TailEvent: typeof TailEvent + TraceEvent: typeof TailEvent + ScheduledEvent: typeof ScheduledEvent + MessageEvent: typeof MessageEvent + CloseEvent: typeof CloseEvent + ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader + ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader + ReadableStream: typeof ReadableStream + WritableStream: typeof WritableStream + WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter + TransformStream: typeof TransformStream + ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy + CountQueuingStrategy: typeof CountQueuingStrategy + ErrorEvent: typeof ErrorEvent + EventSource: typeof EventSource + ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest + ReadableStreamDefaultController: typeof ReadableStreamDefaultController + ReadableByteStreamController: typeof ReadableByteStreamController + WritableStreamDefaultController: typeof WritableStreamDefaultController + TransformStreamDefaultController: typeof TransformStreamDefaultController + CompressionStream: typeof CompressionStream + DecompressionStream: typeof DecompressionStream + TextEncoderStream: typeof TextEncoderStream + TextDecoderStream: typeof TextDecoderStream + Headers: typeof Headers + Body: typeof Body + Request: typeof Request + Response: typeof Response + WebSocket: typeof WebSocket + WebSocketPair: typeof WebSocketPair + WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair + AbortController: typeof AbortController + AbortSignal: typeof AbortSignal + TextDecoder: typeof TextDecoder + TextEncoder: typeof TextEncoder + navigator: Navigator + Navigator: typeof Navigator + URL: typeof URL + URLSearchParams: typeof URLSearchParams + URLPattern: typeof URLPattern + Blob: typeof Blob + File: typeof File + FormData: typeof FormData + Crypto: typeof Crypto + SubtleCrypto: typeof SubtleCrypto + CryptoKey: typeof CryptoKey + CacheStorage: typeof CacheStorage + Cache: typeof Cache + FixedLengthStream: typeof FixedLengthStream + IdentityTransformStream: typeof IdentityTransformStream + HTMLRewriter: typeof HTMLRewriter +} +declare function addEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetAddEventListenerOptions | boolean +): void +declare function removeEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetEventListenerOptions | boolean +): void +/** + * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ +declare function dispatchEvent( + event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap] +): boolean +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args +): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */ +declare function clearTimeout(timeoutId: number | null): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval( + callback: (...args: Args) => void, + msDelay?: number, + ...args: Args +): number +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */ +declare function clearInterval(timeoutId: number | null): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */ +declare function queueMicrotask(task: Function): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */ +declare function structuredClone(value: T, options?: StructuredSerializeOptions): T +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ +declare function reportError(error: any): void +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ +declare function fetch( + input: RequestInfo | URL, + init?: RequestInit +): Promise +declare const self: ServiceWorkerGlobalScope +/** + * The Web Crypto API provides a set of low-level functions for common cryptographic tasks. + * The Workers runtime implements the full surface of this API, but with some differences in + * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) + * compared to those implemented in most browsers. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) + */ +declare const crypto: Crypto +/** + * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) + */ +declare const caches: CacheStorage +declare const scheduler: Scheduler +/** + * The Workers runtime supports a subset of the Performance API, used to measure timing and performance, + * as well as timing of subrequests and other operations. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) + */ +declare const performance: Performance +declare const Cloudflare: Cloudflare +declare const origin: string +declare const navigator: Navigator +interface TestController {} +interface ExecutionContext { + waitUntil(promise: Promise): void + passThroughOnException(): void + props: any +} +type ExportedHandlerFetchHandler = ( + request: Request>, + env: Env, + ctx: ExecutionContext +) => Response | Promise +type ExportedHandlerTailHandler = ( + events: TraceItem[], + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerTraceHandler = ( + traces: TraceItem[], + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerTailStreamHandler = ( + event: TailStream.TailEvent, + env: Env, + ctx: ExecutionContext +) => TailStream.TailEventHandlerType | Promise +type ExportedHandlerScheduledHandler = ( + controller: ScheduledController, + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerQueueHandler = ( + batch: MessageBatch, + env: Env, + ctx: ExecutionContext +) => void | Promise +type ExportedHandlerTestHandler = ( + controller: TestController, + env: Env, + ctx: ExecutionContext +) => void | Promise +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler + tail?: ExportedHandlerTailHandler + trace?: ExportedHandlerTraceHandler + tailStream?: ExportedHandlerTailStreamHandler + scheduled?: ExportedHandlerScheduledHandler + test?: ExportedHandlerTestHandler + email?: EmailExportedHandler + queue?: ExportedHandlerQueueHandler +} +interface StructuredSerializeOptions { + transfer?: any[] +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent) */ +declare abstract class PromiseRejectionEvent extends Event { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise) */ + readonly promise: Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason) */ + readonly reason: any +} +declare abstract class Navigator { + sendBeacon( + url: string, + body?: + | ReadableStream + | string + | (ArrayBuffer | ArrayBufferView) + | Blob + | FormData + | URLSearchParams + | URLSearchParams + ): boolean + readonly userAgent: string + readonly hardwareConcurrency: number +} +/** + * The Workers runtime supports a subset of the Performance API, used to measure timing and performance, + * as well as timing of subrequests and other operations. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) + */ +interface Performance { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */ + readonly timeOrigin: number + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ + now(): number +} +interface AlarmInvocationInfo { + readonly isRetry: boolean + readonly retryCount: number +} +interface Cloudflare { + readonly compatibilityFlags: Record +} +interface DurableObject { + fetch(request: Request): Response | Promise + alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise + webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise + webSocketClose?( + ws: WebSocket, + code: number, + reason: string, + wasClean: boolean + ): void | Promise + webSocketError?(ws: WebSocket, error: unknown): void | Promise +} +type DurableObjectStub = Fetcher< + T, + 'alarm' | 'webSocketMessage' | 'webSocketClose' | 'webSocketError' +> & { + readonly id: DurableObjectId + readonly name?: string +} +interface DurableObjectId { + toString(): string + equals(other: DurableObjectId): boolean + readonly name?: string +} +interface DurableObjectNamespace { + newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId + idFromName(name: string): DurableObjectId + idFromString(id: string): DurableObjectId + get( + id: DurableObjectId, + options?: DurableObjectNamespaceGetDurableObjectOptions + ): DurableObjectStub + jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace +} +type DurableObjectJurisdiction = 'eu' | 'fedramp' | 'fedramp-high' +interface DurableObjectNamespaceNewUniqueIdOptions { + jurisdiction?: DurableObjectJurisdiction +} +type DurableObjectLocationHint = + | 'wnam' + | 'enam' + | 'sam' + | 'weur' + | 'eeur' + | 'apac' + | 'oc' + | 'afr' + | 'me' +interface DurableObjectNamespaceGetDurableObjectOptions { + locationHint?: DurableObjectLocationHint +} +interface DurableObjectState { + waitUntil(promise: Promise): void + readonly id: DurableObjectId + readonly storage: DurableObjectStorage + container?: Container + blockConcurrencyWhile(callback: () => Promise): Promise + acceptWebSocket(ws: WebSocket, tags?: string[]): void + getWebSockets(tag?: string): WebSocket[] + setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void + getWebSocketAutoResponse(): WebSocketRequestResponsePair | null + getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null + setHibernatableWebSocketEventTimeout(timeoutMs?: number): void + getHibernatableWebSocketEventTimeout(): number | null + getTags(ws: WebSocket): string[] + abort(reason?: string): void +} +interface DurableObjectTransaction { + get(key: string, options?: DurableObjectGetOptions): Promise + get(keys: string[], options?: DurableObjectGetOptions): Promise> + list(options?: DurableObjectListOptions): Promise> + put(key: string, value: T, options?: DurableObjectPutOptions): Promise + put(entries: Record, options?: DurableObjectPutOptions): Promise + delete(key: string, options?: DurableObjectPutOptions): Promise + delete(keys: string[], options?: DurableObjectPutOptions): Promise + rollback(): void + getAlarm(options?: DurableObjectGetAlarmOptions): Promise + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise +} +interface DurableObjectStorage { + get(key: string, options?: DurableObjectGetOptions): Promise + get(keys: string[], options?: DurableObjectGetOptions): Promise> + list(options?: DurableObjectListOptions): Promise> + put(key: string, value: T, options?: DurableObjectPutOptions): Promise + put(entries: Record, options?: DurableObjectPutOptions): Promise + delete(key: string, options?: DurableObjectPutOptions): Promise + delete(keys: string[], options?: DurableObjectPutOptions): Promise + deleteAll(options?: DurableObjectPutOptions): Promise + transaction(closure: (txn: DurableObjectTransaction) => Promise): Promise + getAlarm(options?: DurableObjectGetAlarmOptions): Promise + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise + sync(): Promise + sql: SqlStorage + transactionSync(closure: () => T): T + getCurrentBookmark(): Promise + getBookmarkForTime(timestamp: number | Date): Promise + onNextSessionRestoreBookmark(bookmark: string): Promise +} +interface DurableObjectListOptions { + start?: string + startAfter?: string + end?: string + prefix?: string + reverse?: boolean + limit?: number + allowConcurrency?: boolean + noCache?: boolean +} +interface DurableObjectGetOptions { + allowConcurrency?: boolean + noCache?: boolean +} +interface DurableObjectGetAlarmOptions { + allowConcurrency?: boolean +} +interface DurableObjectPutOptions { + allowConcurrency?: boolean + allowUnconfirmed?: boolean + noCache?: boolean +} +interface DurableObjectSetAlarmOptions { + allowConcurrency?: boolean + allowUnconfirmed?: boolean +} +declare class WebSocketRequestResponsePair { + constructor(request: string, response: string) + get request(): string + get response(): string +} +interface AnalyticsEngineDataset { + writeDataPoint(event?: AnalyticsEngineDataPoint): void +} +interface AnalyticsEngineDataPoint { + indexes?: ((ArrayBuffer | string) | null)[] + doubles?: number[] + blobs?: ((ArrayBuffer | string) | null)[] +} +/** + * An event which takes place in the DOM. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event) + */ +declare class Event { + constructor(type: string, init?: EventInit) + /** + * Returns the type of event, e.g. "click", "hashchange", or "submit". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type) + */ + get type(): string + /** + * Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase) + */ + get eventPhase(): number + /** + * Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed) + */ + get composed(): boolean + /** + * Returns true or false depending on how event was initialized. True if event goes through its target's ancestors in reverse tree order, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles) + */ + get bubbles(): boolean + /** + * Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable) + */ + get cancelable(): boolean + /** + * Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented) + */ + get defaultPrevented(): boolean + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue) + */ + get returnValue(): boolean + /** + * Returns the object whose event listener's callback is currently being invoked. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget) + */ + get currentTarget(): EventTarget | undefined + /** + * Returns the object to which event is dispatched (its target). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target) + */ + get target(): EventTarget | undefined + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement) + */ + get srcElement(): EventTarget | undefined + /** + * Returns the event's timestamp as the number of milliseconds measured relative to the time origin. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp) + */ + get timeStamp(): number + /** + * Returns true if event was dispatched by the user agent, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted) + */ + get isTrusted(): boolean + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + get cancelBubble(): boolean + /** + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + set cancelBubble(value: boolean) + /** + * Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation) + */ + stopImmediatePropagation(): void + /** + * If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault) + */ + preventDefault(): void + /** + * When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation) + */ + stopPropagation(): void + /** + * Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath) + */ + composedPath(): EventTarget[] + static readonly NONE: number + static readonly CAPTURING_PHASE: number + static readonly AT_TARGET: number + static readonly BUBBLING_PHASE: number +} +interface EventInit { + bubbles?: boolean + cancelable?: boolean + composed?: boolean +} +type EventListener = (event: EventType) => void +interface EventListenerObject { + handleEvent(event: EventType): void +} +type EventListenerOrEventListenerObject = + | EventListener + | EventListenerObject +/** + * EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget) + */ +declare class EventTarget = Record> { + constructor() + /** + * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. + * + * The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. + * + * When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. + * + * When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners. + * + * When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. + * + * If an AbortSignal is passed for options's signal, then the event listener will be removed when signal is aborted. + * + * The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener) + */ + addEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetAddEventListenerOptions | boolean + ): void + /** + * Removes the event listener in target's event listener list with the same type, callback, and options. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener) + */ + removeEventListener( + type: Type, + handler: EventListenerOrEventListenerObject, + options?: EventTargetEventListenerOptions | boolean + ): void + /** + * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ + dispatchEvent(event: EventMap[keyof EventMap]): boolean +} +interface EventTargetEventListenerOptions { + capture?: boolean +} +interface EventTargetAddEventListenerOptions { + capture?: boolean + passive?: boolean + once?: boolean + signal?: AbortSignal +} +interface EventTargetHandlerObject { + handleEvent: (event: Event) => any | undefined +} +/** + * A controller object that allows you to abort one or more DOM requests as and when desired. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController) + */ +declare class AbortController { + constructor() + /** + * Returns the AbortSignal object associated with this object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal) + */ + get signal(): AbortSignal + /** + * Invoking this method will set this object's AbortSignal's aborted flag and signal to any observers that the associated activity is to be aborted. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort) + */ + abort(reason?: any): void +} +/** + * A signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal) + */ +declare abstract class AbortSignal extends EventTarget { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static) */ + static abort(reason?: any): AbortSignal + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static) */ + static timeout(delay: number): AbortSignal + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static) */ + static any(signals: AbortSignal[]): AbortSignal + /** + * Returns true if this AbortSignal's AbortController has signaled to abort, and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted) + */ + get aborted(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason) */ + get reason(): any + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + get onabort(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + set onabort(value: any | null) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) */ + throwIfAborted(): void +} +interface Scheduler { + wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise +} +interface SchedulerWaitOptions { + signal?: AbortSignal +} +/** + * Extends the lifetime of the install and activate events dispatched on the global scope as part of the service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent) + */ +declare abstract class ExtendableEvent extends Event { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) */ + waitUntil(promise: Promise): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) */ +declare class CustomEvent extends Event { + constructor(type: string, init?: CustomEventCustomEventInit) + /** + * Returns any custom data event was created with. Typically used for synthetic events. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail) + */ + get detail(): T +} +interface CustomEventCustomEventInit { + bubbles?: boolean + cancelable?: boolean + composed?: boolean + detail?: any +} +/** + * A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob) + */ +declare class Blob { + constructor(type?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */ + get size(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */ + get type(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */ + slice(start?: number, end?: number, type?: string): Blob + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer) */ + arrayBuffer(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes) */ + bytes(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */ + text(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream) */ + stream(): ReadableStream +} +interface BlobOptions { + type?: string +} +/** + * Provides information about files and allows JavaScript in a web page to access their content. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File) + */ +declare class File extends Blob { + constructor( + bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, + name: string, + options?: FileOptions + ) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */ + get name(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */ + get lastModified(): number +} +interface FileOptions { + type?: string + lastModified?: number +} +/** + * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) + */ +declare abstract class CacheStorage { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open) */ + open(cacheName: string): Promise + readonly default: Cache +} +/** + * The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) + */ +declare abstract class Cache { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */ + delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */ + match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */ + put(request: RequestInfo | URL, response: Response): Promise +} +interface CacheQueryOptions { + ignoreMethod?: boolean +} +/** + * The Web Crypto API provides a set of low-level functions for common cryptographic tasks. + * The Workers runtime implements the full surface of this API, but with some differences in + * the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) + * compared to those implemented in most browsers. + * + * [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) + */ +declare abstract class Crypto { + /** + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) + */ + get subtle(): SubtleCrypto + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) */ + getRandomValues< + T extends + | Int8Array + | Uint8Array + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >(buffer: T): T + /** + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID) + */ + randomUUID(): string + DigestStream: typeof DigestStream +} +/** + * This Web Crypto API interface provides a number of low-level cryptographic functions. It is accessed via the Crypto.subtle properties available in a window context (via Window.crypto). + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) + */ +declare abstract class SubtleCrypto { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) */ + encrypt( + algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + plainText: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) */ + decrypt( + algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + cipherText: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) */ + sign( + algorithm: string | SubtleCryptoSignAlgorithm, + key: CryptoKey, + data: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) */ + verify( + algorithm: string | SubtleCryptoSignAlgorithm, + key: CryptoKey, + signature: ArrayBuffer | ArrayBufferView, + data: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ + digest( + algorithm: string | SubtleCryptoHashAlgorithm, + data: ArrayBuffer | ArrayBufferView + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) */ + generateKey( + algorithm: string | SubtleCryptoGenerateKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) */ + deriveKey( + algorithm: string | SubtleCryptoDeriveKeyAlgorithm, + baseKey: CryptoKey, + derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) */ + deriveBits( + algorithm: string | SubtleCryptoDeriveKeyAlgorithm, + baseKey: CryptoKey, + length?: number | null + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) */ + importKey( + format: string, + keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, + algorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) */ + exportKey(format: string, key: CryptoKey): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) */ + wrapKey( + format: string, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm + ): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) */ + unwrapKey( + format: string, + wrappedKey: ArrayBuffer | ArrayBufferView, + unwrappingKey: CryptoKey, + unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, + unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[] + ): Promise + timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean +} +/** + * The CryptoKey dictionary of the Web Crypto API represents a cryptographic key. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey) + */ +declare abstract class CryptoKey { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) */ + readonly type: string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) */ + readonly extractable: boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) */ + readonly algorithm: + | CryptoKeyKeyAlgorithm + | CryptoKeyAesKeyAlgorithm + | CryptoKeyHmacKeyAlgorithm + | CryptoKeyRsaKeyAlgorithm + | CryptoKeyEllipticKeyAlgorithm + | CryptoKeyArbitraryKeyAlgorithm + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) */ + readonly usages: string[] +} +interface CryptoKeyPair { + publicKey: CryptoKey + privateKey: CryptoKey +} +interface JsonWebKey { + kty: string + use?: string + key_ops?: string[] + alg?: string + ext?: boolean + crv?: string + x?: string + y?: string + d?: string + n?: string + e?: string + p?: string + q?: string + dp?: string + dq?: string + qi?: string + oth?: RsaOtherPrimesInfo[] + k?: string +} +interface RsaOtherPrimesInfo { + r?: string + d?: string + t?: string +} +interface SubtleCryptoDeriveKeyAlgorithm { + name: string + salt?: ArrayBuffer | ArrayBufferView + iterations?: number + hash?: string | SubtleCryptoHashAlgorithm + $public?: CryptoKey + info?: ArrayBuffer | ArrayBufferView +} +interface SubtleCryptoEncryptAlgorithm { + name: string + iv?: ArrayBuffer | ArrayBufferView + additionalData?: ArrayBuffer | ArrayBufferView + tagLength?: number + counter?: ArrayBuffer | ArrayBufferView + length?: number + label?: ArrayBuffer | ArrayBufferView +} +interface SubtleCryptoGenerateKeyAlgorithm { + name: string + hash?: string | SubtleCryptoHashAlgorithm + modulusLength?: number + publicExponent?: ArrayBuffer | ArrayBufferView + length?: number + namedCurve?: string +} +interface SubtleCryptoHashAlgorithm { + name: string +} +interface SubtleCryptoImportKeyAlgorithm { + name: string + hash?: string | SubtleCryptoHashAlgorithm + length?: number + namedCurve?: string + compressed?: boolean +} +interface SubtleCryptoSignAlgorithm { + name: string + hash?: string | SubtleCryptoHashAlgorithm + dataLength?: number + saltLength?: number +} +interface CryptoKeyKeyAlgorithm { + name: string +} +interface CryptoKeyAesKeyAlgorithm { + name: string + length: number +} +interface CryptoKeyHmacKeyAlgorithm { + name: string + hash: CryptoKeyKeyAlgorithm + length: number +} +interface CryptoKeyRsaKeyAlgorithm { + name: string + modulusLength: number + publicExponent: ArrayBuffer | ArrayBufferView + hash?: CryptoKeyKeyAlgorithm +} +interface CryptoKeyEllipticKeyAlgorithm { + name: string + namedCurve: string +} +interface CryptoKeyArbitraryKeyAlgorithm { + name: string + hash?: CryptoKeyKeyAlgorithm + namedCurve?: string + length?: number +} +declare class DigestStream extends WritableStream { + constructor(algorithm: string | SubtleCryptoHashAlgorithm) + readonly digest: Promise + get bytesWritten(): number | bigint +} +/** + * A decoder for a specific method, that is a specific character encoding, like utf-8, iso-8859-2, koi8, cp1261, gbk, etc. A decoder takes a stream of bytes as input and emits a stream of code points. For a more scalable, non-native library, see StringView – a C-like representation of strings based on typed arrays. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) + */ +declare class TextDecoder { + constructor(label?: string, options?: TextDecoderConstructorOptions) + /** + * Returns the result of running encoding's decoder. The method can be invoked zero or more times with options's stream set to true, and then once without options's stream (or set to false), to process a fragmented input. If the invocation without options's stream (or set to false) has no input, it's clearest to omit both arguments. + * + * ``` + * var string = "", decoder = new TextDecoder(encoding), buffer; + * while(buffer = next_chunk()) { + * string += decoder.decode(buffer, {stream:true}); + * } + * string += decoder.decode(); // end-of-queue + * ``` + * + * If the error mode is "fatal" and encoding's decoder returns error, throws a TypeError. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) + */ + decode(input?: ArrayBuffer | ArrayBufferView, options?: TextDecoderDecodeOptions): string + get encoding(): string + get fatal(): boolean + get ignoreBOM(): boolean +} +/** + * TextEncoder takes a stream of code points as input and emits a stream of bytes. For a more scalable, non-native library, see StringView – a C-like representation of strings based on typed arrays. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) + */ +declare class TextEncoder { + constructor() + /** + * Returns the result of running UTF-8's encoder. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) + */ + encode(input?: string): Uint8Array + /** + * Runs the UTF-8 encoder on source, stores the result of that operation into destination, and returns the progress made as an object wherein read is the number of converted code units of source and written is the number of bytes modified in destination. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) + */ + encodeInto(input: string, buffer: ArrayBuffer | ArrayBufferView): TextEncoderEncodeIntoResult + get encoding(): string +} +interface TextDecoderConstructorOptions { + fatal: boolean + ignoreBOM: boolean +} +interface TextDecoderDecodeOptions { + stream: boolean +} +interface TextEncoderEncodeIntoResult { + read: number + written: number +} +/** + * Events providing information related to errors in scripts or in files. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent) + */ +declare class ErrorEvent extends Event { + constructor(type: string, init?: ErrorEventErrorEventInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename) */ + get filename(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message) */ + get message(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno) */ + get lineno(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno) */ + get colno(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error) */ + get error(): any +} +interface ErrorEventErrorEventInit { + message?: string + filename?: string + lineno?: number + colno?: number + error?: any +} +/** + * Provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData) + */ +declare class FormData { + constructor() + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) */ + append(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) */ + append(name: string, value: Blob, filename?: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete) */ + delete(name: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get) */ + get(name: string): (File | string) | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll) */ + getAll(name: string): (File | string)[] + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) */ + has(name: string): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) */ + set(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) */ + set(name: string, value: Blob, filename?: string): void + /* Returns an array of key, value pairs for every entry in the list. */ + entries(): IterableIterator<[key: string, value: File | string]> + /* Returns a list of keys in the list. */ + keys(): IterableIterator + /* Returns a list of values in the list. */ + values(): IterableIterator + forEach( + callback: (this: This, value: File | string, key: string, parent: FormData) => void, + thisArg?: This + ): void + [Symbol.iterator](): IterableIterator<[key: string, value: File | string]> +} +interface ContentOptions { + html?: boolean +} +declare class HTMLRewriter { + constructor() + on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter + onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter + transform(response: Response): Response +} +interface HTMLRewriterElementContentHandlers { + element?(element: Element): void | Promise + comments?(comment: Comment): void | Promise + text?(element: Text): void | Promise +} +interface HTMLRewriterDocumentContentHandlers { + doctype?(doctype: Doctype): void | Promise + comments?(comment: Comment): void | Promise + text?(text: Text): void | Promise + end?(end: DocumentEnd): void | Promise +} +interface Doctype { + readonly name: string | null + readonly publicId: string | null + readonly systemId: string | null +} +interface Element { + tagName: string + readonly attributes: IterableIterator + readonly removed: boolean + readonly namespaceURI: string + getAttribute(name: string): string | null + hasAttribute(name: string): boolean + setAttribute(name: string, value: string): Element + removeAttribute(name: string): Element + before(content: string | ReadableStream | Response, options?: ContentOptions): Element + after(content: string | ReadableStream | Response, options?: ContentOptions): Element + prepend(content: string | ReadableStream | Response, options?: ContentOptions): Element + append(content: string | ReadableStream | Response, options?: ContentOptions): Element + replace(content: string | ReadableStream | Response, options?: ContentOptions): Element + remove(): Element + removeAndKeepContent(): Element + setInnerContent(content: string | ReadableStream | Response, options?: ContentOptions): Element + onEndTag(handler: (tag: EndTag) => void | Promise): void +} +interface EndTag { + name: string + before(content: string | ReadableStream | Response, options?: ContentOptions): EndTag + after(content: string | ReadableStream | Response, options?: ContentOptions): EndTag + remove(): EndTag +} +interface Comment { + text: string + readonly removed: boolean + before(content: string, options?: ContentOptions): Comment + after(content: string, options?: ContentOptions): Comment + replace(content: string, options?: ContentOptions): Comment + remove(): Comment +} +interface Text { + readonly text: string + readonly lastInTextNode: boolean + readonly removed: boolean + before(content: string | ReadableStream | Response, options?: ContentOptions): Text + after(content: string | ReadableStream | Response, options?: ContentOptions): Text + replace(content: string | ReadableStream | Response, options?: ContentOptions): Text + remove(): Text +} +interface DocumentEnd { + append(content: string, options?: ContentOptions): DocumentEnd +} +/** + * This is the event type for fetch events dispatched on the service worker global scope. It contains information about the fetch, including the request and how the receiver will treat the response. It provides the event.respondWith() method, which allows us to provide a response to this fetch. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent) + */ +declare abstract class FetchEvent extends ExtendableEvent { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request) */ + readonly request: Request + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/respondWith) */ + respondWith(promise: Response | Promise): void + passThroughOnException(): void +} +type HeadersInit = Headers | Iterable> | Record +/** + * This Fetch API interface allows you to perform various actions on HTTP request and response headers. These actions include retrieving, setting, adding to, and removing. A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs.  You can add to this using methods like append() (see Examples.) In all methods of this interface, header names are matched by case-insensitive byte sequence. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers) + */ +declare class Headers { + constructor(init?: HeadersInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) */ + get(name: string): string | null + getAll(name: string): string[] + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) */ + getSetCookie(): string[] + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) */ + has(name: string): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) */ + set(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) */ + append(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) */ + delete(name: string): void + forEach( + callback: (this: This, value: string, key: string, parent: Headers) => void, + thisArg?: This + ): void + /* Returns an iterator allowing to go through all key/value pairs contained in this object. */ + entries(): IterableIterator<[key: string, value: string]> + /* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. */ + keys(): IterableIterator + /* Returns an iterator allowing to go through all values of the key/value pairs contained in this object. */ + values(): IterableIterator + [Symbol.iterator](): IterableIterator<[key: string, value: string]> +} +type BodyInit = + | ReadableStream + | string + | ArrayBuffer + | ArrayBufferView + | Blob + | URLSearchParams + | FormData +declare abstract class Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ + get body(): ReadableStream | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + get bodyUsed(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */ + bytes(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */ + text(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */ + json(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */ + formData(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob(): Promise +} +/** + * This Fetch API interface represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +declare var Response: { + prototype: Response + new (body?: BodyInit | null, init?: ResponseInit): Response + error(): Response + redirect(url: string, status?: number): Response + json(any: any, maybeInit?: ResponseInit | Response): Response +} +/** + * This Fetch API interface represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +interface Response extends Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/clone) */ + clone(): Response + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status) */ + status: number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText) */ + statusText: string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) */ + headers: Headers + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok) */ + ok: boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/redirected) */ + redirected: boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/url) */ + url: string + webSocket: WebSocket | null + cf: any | undefined + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/type) */ + type: 'default' | 'error' +} +interface ResponseInit { + status?: number + statusText?: string + headers?: HeadersInit + cf?: any + webSocket?: WebSocket | null + encodeBody?: 'automatic' | 'manual' +} +type RequestInfo> = + | Request + | string +/** + * This Fetch API interface represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +declare var Request: { + prototype: Request + new >( + input: RequestInfo | URL, + init?: RequestInit + ): Request +} +/** + * This Fetch API interface represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +interface Request> extends Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/clone) */ + clone(): Request + /** + * Returns request's HTTP method, which is "GET" by default. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/method) + */ + method: string + /** + * Returns the URL of request as a string. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url) + */ + url: string + /** + * Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers) + */ + headers: Headers + /** + * Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/redirect) + */ + redirect: string + fetcher: Fetcher | null + /** + * Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) + */ + signal: AbortSignal + cf: Cf | undefined + /** + * Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity) + */ + integrity: string + /** + * Returns a boolean indicating whether or not request can outlive the global in which it was created. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive) + */ + keepalive: boolean + /** + * Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache) + */ + cache?: 'no-store' +} +interface RequestInit { + /* A string to set request's method. */ + method?: string + /* A Headers object, an object literal, or an array of two-item arrays to set request's headers. */ + headers?: HeadersInit + /* A BodyInit object or null to set request's body. */ + body?: BodyInit | null + /* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */ + redirect?: string + fetcher?: Fetcher | null + cf?: Cf + /* A string indicating how the request will interact with the browser's cache to set request's cache. */ + cache?: 'no-store' + /* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */ + integrity?: string + /* An AbortSignal to set request's signal. */ + signal?: AbortSignal | null + encodeResponseBody?: 'automatic' | 'manual' +} +type Service = Fetcher +type Fetcher< + T extends Rpc.EntrypointBranded | undefined = undefined, + Reserved extends string = never, +> = (T extends Rpc.EntrypointBranded + ? Rpc.Provider + : unknown) & { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise + connect(address: SocketAddress | string, options?: SocketOptions): Socket +} +interface KVNamespaceListKey { + name: Key + expiration?: number + metadata?: Metadata +} +type KVNamespaceListResult = + | { + list_complete: false + keys: KVNamespaceListKey[] + cursor: string + cacheStatus: string | null + } + | { + list_complete: true + keys: KVNamespaceListKey[] + cacheStatus: string | null + } +interface KVNamespace { + get(key: Key, options?: Partial>): Promise + get(key: Key, type: 'text'): Promise + get(key: Key, type: 'json'): Promise + get(key: Key, type: 'arrayBuffer'): Promise + get(key: Key, type: 'stream'): Promise + get(key: Key, options?: KVNamespaceGetOptions<'text'>): Promise + get( + key: Key, + options?: KVNamespaceGetOptions<'json'> + ): Promise + get(key: Key, options?: KVNamespaceGetOptions<'arrayBuffer'>): Promise + get(key: Key, options?: KVNamespaceGetOptions<'stream'>): Promise + get(key: Array, type: 'text'): Promise> + get( + key: Array, + type: 'json' + ): Promise> + get( + key: Array, + options?: Partial> + ): Promise> + get(key: Array, options?: KVNamespaceGetOptions<'text'>): Promise> + get( + key: Array, + options?: KVNamespaceGetOptions<'json'> + ): Promise> + list( + options?: KVNamespaceListOptions + ): Promise> + put( + key: Key, + value: string | ArrayBuffer | ArrayBufferView | ReadableStream, + options?: KVNamespacePutOptions + ): Promise + getWithMetadata( + key: Key, + options?: Partial> + ): Promise> + getWithMetadata( + key: Key, + type: 'text' + ): Promise> + getWithMetadata( + key: Key, + type: 'json' + ): Promise> + getWithMetadata( + key: Key, + type: 'arrayBuffer' + ): Promise> + getWithMetadata( + key: Key, + type: 'stream' + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'text'> + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'json'> + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'arrayBuffer'> + ): Promise> + getWithMetadata( + key: Key, + options: KVNamespaceGetOptions<'stream'> + ): Promise> + getWithMetadata( + key: Array, + type: 'text' + ): Promise>> + getWithMetadata( + key: Array, + type: 'json' + ): Promise>> + getWithMetadata( + key: Array, + options?: Partial> + ): Promise>> + getWithMetadata( + key: Array, + options?: KVNamespaceGetOptions<'text'> + ): Promise>> + getWithMetadata( + key: Array, + options?: KVNamespaceGetOptions<'json'> + ): Promise>> + delete(key: Key): Promise +} +interface KVNamespaceListOptions { + limit?: number + prefix?: string | null + cursor?: string | null +} +interface KVNamespaceGetOptions { + type: Type + cacheTtl?: number +} +interface KVNamespacePutOptions { + expiration?: number + expirationTtl?: number + metadata?: any | null +} +interface KVNamespaceGetWithMetadataResult { + value: Value | null + metadata: Metadata | null + cacheStatus: string | null +} +type QueueContentType = 'text' | 'bytes' | 'json' | 'v8' +interface Queue { + send(message: Body, options?: QueueSendOptions): Promise + sendBatch( + messages: Iterable>, + options?: QueueSendBatchOptions + ): Promise +} +interface QueueSendOptions { + contentType?: QueueContentType + delaySeconds?: number +} +interface QueueSendBatchOptions { + delaySeconds?: number +} +interface MessageSendRequest { + body: Body + contentType?: QueueContentType + delaySeconds?: number +} +interface QueueRetryOptions { + delaySeconds?: number +} +interface Message { + readonly id: string + readonly timestamp: Date + readonly body: Body + readonly attempts: number + retry(options?: QueueRetryOptions): void + ack(): void +} +interface QueueEvent extends ExtendableEvent { + readonly messages: readonly Message[] + readonly queue: string + retryAll(options?: QueueRetryOptions): void + ackAll(): void +} +interface MessageBatch { + readonly messages: readonly Message[] + readonly queue: string + retryAll(options?: QueueRetryOptions): void + ackAll(): void +} +interface R2Error extends Error { + readonly name: string + readonly code: number + readonly message: string + readonly action: string + readonly stack: any +} +interface R2ListOptions { + limit?: number + prefix?: string + cursor?: string + delimiter?: string + startAfter?: string + include?: ('httpMetadata' | 'customMetadata')[] +} +declare abstract class R2Bucket { + head(key: string): Promise + get( + key: string, + options: R2GetOptions & { + onlyIf: R2Conditional | Headers + } + ): Promise + get(key: string, options?: R2GetOptions): Promise + put( + key: string, + value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, + options?: R2PutOptions & { + onlyIf: R2Conditional | Headers + } + ): Promise + put( + key: string, + value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, + options?: R2PutOptions + ): Promise + createMultipartUpload(key: string, options?: R2MultipartOptions): Promise + resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload + delete(keys: string | string[]): Promise + list(options?: R2ListOptions): Promise +} +interface R2MultipartUpload { + readonly key: string + readonly uploadId: string + uploadPart( + partNumber: number, + value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob, + options?: R2UploadPartOptions + ): Promise + abort(): Promise + complete(uploadedParts: R2UploadedPart[]): Promise +} +interface R2UploadedPart { + partNumber: number + etag: string +} +declare abstract class R2Object { + readonly key: string + readonly version: string + readonly size: number + readonly etag: string + readonly httpEtag: string + readonly checksums: R2Checksums + readonly uploaded: Date + readonly httpMetadata?: R2HTTPMetadata + readonly customMetadata?: Record + readonly range?: R2Range + readonly storageClass: string + readonly ssecKeyMd5?: string + writeHttpMetadata(headers: Headers): void +} +interface R2ObjectBody extends R2Object { + get body(): ReadableStream + get bodyUsed(): boolean + arrayBuffer(): Promise + bytes(): Promise + text(): Promise + json(): Promise + blob(): Promise +} +type R2Range = + | { + offset: number + length?: number + } + | { + offset?: number + length: number + } + | { + suffix: number + } +interface R2Conditional { + etagMatches?: string + etagDoesNotMatch?: string + uploadedBefore?: Date + uploadedAfter?: Date + secondsGranularity?: boolean +} +interface R2GetOptions { + onlyIf?: R2Conditional | Headers + range?: R2Range | Headers + ssecKey?: ArrayBuffer | string +} +interface R2PutOptions { + onlyIf?: R2Conditional | Headers + httpMetadata?: R2HTTPMetadata | Headers + customMetadata?: Record + md5?: (ArrayBuffer | ArrayBufferView) | string + sha1?: (ArrayBuffer | ArrayBufferView) | string + sha256?: (ArrayBuffer | ArrayBufferView) | string + sha384?: (ArrayBuffer | ArrayBufferView) | string + sha512?: (ArrayBuffer | ArrayBufferView) | string + storageClass?: string + ssecKey?: ArrayBuffer | string +} +interface R2MultipartOptions { + httpMetadata?: R2HTTPMetadata | Headers + customMetadata?: Record + storageClass?: string + ssecKey?: ArrayBuffer | string +} +interface R2Checksums { + readonly md5?: ArrayBuffer + readonly sha1?: ArrayBuffer + readonly sha256?: ArrayBuffer + readonly sha384?: ArrayBuffer + readonly sha512?: ArrayBuffer + toJSON(): R2StringChecksums +} +interface R2StringChecksums { + md5?: string + sha1?: string + sha256?: string + sha384?: string + sha512?: string +} +interface R2HTTPMetadata { + contentType?: string + contentLanguage?: string + contentDisposition?: string + contentEncoding?: string + cacheControl?: string + cacheExpiry?: Date +} +type R2Objects = { + objects: R2Object[] + delimitedPrefixes: string[] +} & ( + | { + truncated: true + cursor: string + } + | { + truncated: false + } +) +interface R2UploadPartOptions { + ssecKey?: ArrayBuffer | string +} +declare abstract class ScheduledEvent extends ExtendableEvent { + readonly scheduledTime: number + readonly cron: string + noRetry(): void +} +interface ScheduledController { + readonly scheduledTime: number + readonly cron: string + noRetry(): void +} +interface QueuingStrategy { + highWaterMark?: number | bigint + size?: (chunk: T) => number | bigint +} +interface UnderlyingSink { + type?: string + start?: (controller: WritableStreamDefaultController) => void | Promise + write?: (chunk: W, controller: WritableStreamDefaultController) => void | Promise + abort?: (reason: any) => void | Promise + close?: () => void | Promise +} +interface UnderlyingByteSource { + type: 'bytes' + autoAllocateChunkSize?: number + start?: (controller: ReadableByteStreamController) => void | Promise + pull?: (controller: ReadableByteStreamController) => void | Promise + cancel?: (reason: any) => void | Promise +} +interface UnderlyingSource { + type?: '' | undefined + start?: (controller: ReadableStreamDefaultController) => void | Promise + pull?: (controller: ReadableStreamDefaultController) => void | Promise + cancel?: (reason: any) => void | Promise + expectedLength?: number | bigint +} +interface Transformer { + readableType?: string + writableType?: string + start?: (controller: TransformStreamDefaultController) => void | Promise + transform?: (chunk: I, controller: TransformStreamDefaultController) => void | Promise + flush?: (controller: TransformStreamDefaultController) => void | Promise + cancel?: (reason: any) => void | Promise + expectedLength?: number +} +interface StreamPipeOptions { + /** + * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + * + * Errors and closures of the source and destination streams propagate as follows: + * + * An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination. + * + * An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source. + * + * When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error. + * + * If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source. + * + * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. + */ + preventClose?: boolean + preventAbort?: boolean + preventCancel?: boolean + signal?: AbortSignal +} +type ReadableStreamReadResult = + | { + done: false + value: R + } + | { + done: true + value?: undefined + } +/** + * This Streams API interface represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +interface ReadableStream { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) */ + get locked(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel) */ + cancel(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) */ + getReader(): ReadableStreamDefaultReader + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) */ + getReader(options: ReadableStreamGetReaderOptions): ReadableStreamBYOBReader + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough) */ + pipeThrough( + transform: ReadableWritablePair, + options?: StreamPipeOptions + ): ReadableStream + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo) */ + pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee) */ + tee(): [ReadableStream, ReadableStream] + values(options?: ReadableStreamValuesOptions): AsyncIterableIterator + [Symbol.asyncIterator](options?: ReadableStreamValuesOptions): AsyncIterableIterator +} +/** + * This Streams API interface represents a readable stream of byte data. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +declare const ReadableStream: { + prototype: ReadableStream + new ( + underlyingSource: UnderlyingByteSource, + strategy?: QueuingStrategy + ): ReadableStream + new ( + underlyingSource?: UnderlyingSource, + strategy?: QueuingStrategy + ): ReadableStream +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader) */ +declare class ReadableStreamDefaultReader { + constructor(stream: ReadableStream) + get closed(): Promise + cancel(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/read) */ + read(): Promise> + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/releaseLock) */ + releaseLock(): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader) */ +declare class ReadableStreamBYOBReader { + constructor(stream: ReadableStream) + get closed(): Promise + cancel(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) */ + read(view: T): Promise> + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) */ + releaseLock(): void + readAtLeast( + minElements: number, + view: T + ): Promise> +} +interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions { + min?: number +} +interface ReadableStreamGetReaderOptions { + /** + * Creates a ReadableStreamBYOBReader and locks the stream to the new reader. + * + * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation. + */ + mode: 'byob' +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest) */ +declare abstract class ReadableStreamBYOBRequest { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view) */ + get view(): Uint8Array | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond) */ + respond(bytesWritten: number): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView) */ + respondWithNewView(view: ArrayBuffer | ArrayBufferView): void + get atLeast(): number | null +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController) */ +declare abstract class ReadableStreamDefaultController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/close) */ + close(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue) */ + enqueue(chunk?: R): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/error) */ + error(reason: any): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController) */ +declare abstract class ReadableByteStreamController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/byobRequest) */ + get byobRequest(): ReadableStreamBYOBRequest | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/close) */ + close(): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/enqueue) */ + enqueue(chunk: ArrayBuffer | ArrayBufferView): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/error) */ + error(reason: any): void +} +/** + * This Streams API interface represents a controller allowing control of a WritableStream's state. When constructing a WritableStream, the underlying sink is given a corresponding WritableStreamDefaultController instance to manipulate. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController) + */ +declare abstract class WritableStreamDefaultController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal) */ + get signal(): AbortSignal + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error) */ + error(reason?: any): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController) */ +declare abstract class TransformStreamDefaultController { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue) */ + enqueue(chunk?: O): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/error) */ + error(reason: any): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate) */ + terminate(): void +} +interface ReadableWritablePair { + /** + * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + */ + writable: WritableStream + readable: ReadableStream +} +/** + * This Streams API interface provides a standard abstraction for writing streaming data to a destination, known as a sink. This object comes with built-in backpressure and queuing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream) + */ +declare class WritableStream { + constructor(underlyingSink?: UnderlyingSink, queuingStrategy?: QueuingStrategy) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/locked) */ + get locked(): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/abort) */ + abort(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/close) */ + close(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/getWriter) */ + getWriter(): WritableStreamDefaultWriter +} +/** + * This Streams API interface is the object returned by WritableStream.getWriter() and once created locks the < writer to the WritableStream ensuring that no other streams can write to the underlying sink. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter) + */ +declare class WritableStreamDefaultWriter { + constructor(stream: WritableStream) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed) */ + get closed(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready) */ + get ready(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize) */ + get desiredSize(): number | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort) */ + abort(reason?: any): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close) */ + close(): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write) */ + write(chunk?: W): Promise + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock) */ + releaseLock(): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream) */ +declare class TransformStream { + constructor( + transformer?: Transformer, + writableStrategy?: QueuingStrategy, + readableStrategy?: QueuingStrategy + ) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/readable) */ + get readable(): ReadableStream + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/writable) */ + get writable(): WritableStream +} +declare class FixedLengthStream extends IdentityTransformStream { + constructor( + expectedLength: number | bigint, + queuingStrategy?: IdentityTransformStreamQueuingStrategy + ) +} +declare class IdentityTransformStream extends TransformStream< + ArrayBuffer | ArrayBufferView, + Uint8Array +> { + constructor(queuingStrategy?: IdentityTransformStreamQueuingStrategy) +} +interface IdentityTransformStreamQueuingStrategy { + highWaterMark?: number | bigint +} +interface ReadableStreamValuesOptions { + preventCancel?: boolean +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CompressionStream) */ +declare class CompressionStream extends TransformStream { + constructor(format: 'gzip' | 'deflate' | 'deflate-raw') +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/DecompressionStream) */ +declare class DecompressionStream extends TransformStream< + ArrayBuffer | ArrayBufferView, + Uint8Array +> { + constructor(format: 'gzip' | 'deflate' | 'deflate-raw') +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoderStream) */ +declare class TextEncoderStream extends TransformStream { + constructor() + get encoding(): string +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoderStream) */ +declare class TextDecoderStream extends TransformStream { + constructor(label?: string, options?: TextDecoderStreamTextDecoderStreamInit) + get encoding(): string + get fatal(): boolean + get ignoreBOM(): boolean +} +interface TextDecoderStreamTextDecoderStreamInit { + fatal?: boolean + ignoreBOM?: boolean +} +/** + * This Streams API interface provides a built-in byte length queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy) + */ +declare class ByteLengthQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/highWaterMark) */ + get highWaterMark(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/size) */ + get size(): (chunk?: any) => number +} +/** + * This Streams API interface provides a built-in byte length queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy) + */ +declare class CountQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/highWaterMark) */ + get highWaterMark(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/size) */ + get size(): (chunk?: any) => number +} +interface QueuingStrategyInit { + /** + * Creates a new ByteLengthQueuingStrategy with the provided high water mark. + * + * Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw. + */ + highWaterMark: number +} +interface ScriptVersion { + id?: string + tag?: string + message?: string +} +declare abstract class TailEvent extends ExtendableEvent { + readonly events: TraceItem[] + readonly traces: TraceItem[] +} +interface TraceItem { + readonly event: + | ( + | TraceItemFetchEventInfo + | TraceItemJsRpcEventInfo + | TraceItemScheduledEventInfo + | TraceItemAlarmEventInfo + | TraceItemQueueEventInfo + | TraceItemEmailEventInfo + | TraceItemTailEventInfo + | TraceItemCustomEventInfo + | TraceItemHibernatableWebSocketEventInfo + ) + | null + readonly eventTimestamp: number | null + readonly logs: TraceLog[] + readonly exceptions: TraceException[] + readonly diagnosticsChannelEvents: TraceDiagnosticChannelEvent[] + readonly scriptName: string | null + readonly entrypoint?: string + readonly scriptVersion?: ScriptVersion + readonly dispatchNamespace?: string + readonly scriptTags?: string[] + readonly outcome: string + readonly executionModel: string + readonly truncated: boolean + readonly cpuTime: number + readonly wallTime: number +} +interface TraceItemAlarmEventInfo { + readonly scheduledTime: Date +} +interface TraceItemCustomEventInfo {} +interface TraceItemScheduledEventInfo { + readonly scheduledTime: number + readonly cron: string +} +interface TraceItemQueueEventInfo { + readonly queue: string + readonly batchSize: number +} +interface TraceItemEmailEventInfo { + readonly mailFrom: string + readonly rcptTo: string + readonly rawSize: number +} +interface TraceItemTailEventInfo { + readonly consumedEvents: TraceItemTailEventInfoTailItem[] +} +interface TraceItemTailEventInfoTailItem { + readonly scriptName: string | null +} +interface TraceItemFetchEventInfo { + readonly response?: TraceItemFetchEventInfoResponse + readonly request: TraceItemFetchEventInfoRequest +} +interface TraceItemFetchEventInfoRequest { + readonly cf?: any + readonly headers: Record + readonly method: string + readonly url: string + getUnredacted(): TraceItemFetchEventInfoRequest +} +interface TraceItemFetchEventInfoResponse { + readonly status: number +} +interface TraceItemJsRpcEventInfo { + readonly rpcMethod: string +} +interface TraceItemHibernatableWebSocketEventInfo { + readonly getWebSocketEvent: + | TraceItemHibernatableWebSocketEventInfoMessage + | TraceItemHibernatableWebSocketEventInfoClose + | TraceItemHibernatableWebSocketEventInfoError +} +interface TraceItemHibernatableWebSocketEventInfoMessage { + readonly webSocketEventType: string +} +interface TraceItemHibernatableWebSocketEventInfoClose { + readonly webSocketEventType: string + readonly code: number + readonly wasClean: boolean +} +interface TraceItemHibernatableWebSocketEventInfoError { + readonly webSocketEventType: string +} +interface TraceLog { + readonly timestamp: number + readonly level: string + readonly message: any +} +interface TraceException { + readonly timestamp: number + readonly message: string + readonly name: string + readonly stack?: string +} +interface TraceDiagnosticChannelEvent { + readonly timestamp: number + readonly channel: string + readonly message: any +} +interface TraceMetrics { + readonly cpuTime: number + readonly wallTime: number +} +interface UnsafeTraceMetrics { + fromTrace(item: TraceItem): TraceMetrics +} +/** + * The URL interface represents an object providing static methods used for creating object URLs. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL) + */ +declare class URL { + constructor(url: string | URL, base?: string | URL) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin) */ + get origin(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) */ + get href(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) */ + set href(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) */ + get protocol(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) */ + set protocol(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) */ + get username(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) */ + set username(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) */ + get password(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) */ + set password(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) */ + get host(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) */ + set host(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) */ + get hostname(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) */ + set hostname(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) */ + get port(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) */ + set port(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) */ + get pathname(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) */ + set pathname(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) */ + get search(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) */ + set search(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) */ + get hash(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) */ + set hash(value: string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams) */ + get searchParams(): URLSearchParams + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON) */ + toJSON(): string + /*function toString() { [native code] }*/ + toString(): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) */ + static canParse(url: string, base?: string): boolean + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/parse_static) */ + static parse(url: string, base?: string): URL | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static) */ + static createObjectURL(object: File | Blob): string + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static) */ + static revokeObjectURL(object_url: string): void +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams) */ +declare class URLSearchParams { + constructor(init?: Iterable> | Record | string) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) */ + get size(): number + /** + * Appends a specified key/value pair as a new search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append) + */ + append(name: string, value: string): void + /** + * Deletes the given search parameter, and its associated value, from the list of all search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete) + */ + delete(name: string, value?: string): void + /** + * Returns the first value associated to the given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) + */ + get(name: string): string | null + /** + * Returns all the values association with a given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) + */ + getAll(name: string): string[] + /** + * Returns a Boolean indicating if such a search parameter exists. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) + */ + has(name: string, value?: string): boolean + /** + * Sets the value associated to a given search parameter to the given value. If there were several values, delete the others. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) + */ + set(name: string, value: string): void + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) */ + sort(): void + /* Returns an array of key, value pairs for every entry in the search params. */ + entries(): IterableIterator<[key: string, value: string]> + /* Returns a list of keys in the search params. */ + keys(): IterableIterator + /* Returns a list of values in the search params. */ + values(): IterableIterator + forEach( + callback: (this: This, value: string, key: string, parent: URLSearchParams) => void, + thisArg?: This + ): void + /*function toString() { [native code] } Returns a string containing a query string suitable for use in a URL. Does not include the question mark. */ + toString(): string + [Symbol.iterator](): IterableIterator<[key: string, value: string]> +} +declare class URLPattern { + constructor( + input?: string | URLPatternInit, + baseURL?: string | URLPatternOptions, + patternOptions?: URLPatternOptions + ) + get protocol(): string + get username(): string + get password(): string + get hostname(): string + get port(): string + get pathname(): string + get search(): string + get hash(): string + get hasRegExpGroups(): boolean + test(input?: string | URLPatternInit, baseURL?: string): boolean + exec(input?: string | URLPatternInit, baseURL?: string): URLPatternResult | null +} +interface URLPatternInit { + protocol?: string + username?: string + password?: string + hostname?: string + port?: string + pathname?: string + search?: string + hash?: string + baseURL?: string +} +interface URLPatternComponentResult { + input: string + groups: Record +} +interface URLPatternResult { + inputs: (string | URLPatternInit)[] + protocol: URLPatternComponentResult + username: URLPatternComponentResult + password: URLPatternComponentResult + hostname: URLPatternComponentResult + port: URLPatternComponentResult + pathname: URLPatternComponentResult + search: URLPatternComponentResult + hash: URLPatternComponentResult +} +interface URLPatternOptions { + ignoreCase?: boolean +} +/** + * A CloseEvent is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent) + */ +declare class CloseEvent extends Event { + constructor(type: string, initializer?: CloseEventInit) + /** + * Returns the WebSocket connection close code provided by the server. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code) + */ + readonly code: number + /** + * Returns the WebSocket connection close reason provided by the server. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason) + */ + readonly reason: string + /** + * Returns true if the connection closed cleanly; false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean) + */ + readonly wasClean: boolean +} +interface CloseEventInit { + code?: number + reason?: string + wasClean?: boolean +} +/** + * A message received by a target object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) + */ +declare class MessageEvent extends Event { + constructor(type: string, initializer: MessageEventInit) + /** + * Returns the data of the message. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) + */ + readonly data: ArrayBuffer | string +} +interface MessageEventInit { + data: ArrayBuffer | string +} +type WebSocketEventMap = { + close: CloseEvent + message: MessageEvent + open: Event + error: ErrorEvent +} +/** + * Provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +declare var WebSocket: { + prototype: WebSocket + new (url: string, protocols?: string[] | string): WebSocket + readonly READY_STATE_CONNECTING: number + readonly CONNECTING: number + readonly READY_STATE_OPEN: number + readonly OPEN: number + readonly READY_STATE_CLOSING: number + readonly CLOSING: number + readonly READY_STATE_CLOSED: number + readonly CLOSED: number +} +/** + * Provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +interface WebSocket extends EventTarget { + accept(): void + /** + * Transmits data using the WebSocket connection. data can be a string, a Blob, an ArrayBuffer, or an ArrayBufferView. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/send) + */ + send(message: (ArrayBuffer | ArrayBufferView) | string): void + /** + * Closes the WebSocket connection, optionally using code as the the WebSocket connection close code and reason as the the WebSocket connection close reason. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/close) + */ + close(code?: number, reason?: string): void + serializeAttachment(attachment: any): void + deserializeAttachment(): any | null + /** + * Returns the state of the WebSocket object's connection. It can have the values described below. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/readyState) + */ + readyState: number + /** + * Returns the URL that was used to establish the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/url) + */ + url: string | null + /** + * Returns the subprotocol selected by the server, if any. It can be used in conjunction with the array form of the constructor's second argument to perform subprotocol negotiation. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/protocol) + */ + protocol: string | null + /** + * Returns the extensions selected by the server, if any. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) + */ + extensions: string | null +} +declare const WebSocketPair: { + new (): { + 0: WebSocket + 1: WebSocket + } +} +interface SqlStorage { + exec>( + query: string, + ...bindings: any[] + ): SqlStorageCursor + get databaseSize(): number + Cursor: typeof SqlStorageCursor + Statement: typeof SqlStorageStatement +} +declare abstract class SqlStorageStatement {} +type SqlStorageValue = ArrayBuffer | string | number | null +declare abstract class SqlStorageCursor> { + next(): + | { + done?: false + value: T + } + | { + done: true + value?: never + } + toArray(): T[] + one(): T + raw(): IterableIterator + columnNames: string[] + get rowsRead(): number + get rowsWritten(): number + [Symbol.iterator](): IterableIterator +} +interface Socket { + get readable(): ReadableStream + get writable(): WritableStream + get closed(): Promise + get opened(): Promise + get upgraded(): boolean + get secureTransport(): 'on' | 'off' | 'starttls' + close(): Promise + startTls(options?: TlsOptions): Socket +} +interface SocketOptions { + secureTransport?: string + allowHalfOpen: boolean + highWaterMark?: number | bigint +} +interface SocketAddress { + hostname: string + port: number +} +interface TlsOptions { + expectedServerHostname?: string +} +interface SocketInfo { + remoteAddress?: string + localAddress?: string +} +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource) */ +declare class EventSource extends EventTarget { + constructor(url: string, init?: EventSourceEventSourceInit) + /** + * Aborts any instances of the fetch algorithm started for this EventSource object, and sets the readyState attribute to CLOSED. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close) + */ + close(): void + /** + * Returns the URL providing the event stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url) + */ + get url(): string + /** + * Returns true if the credentials mode for connection requests to the URL providing the event stream is set to "include", and false otherwise. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials) + */ + get withCredentials(): boolean + /** + * Returns the state of this EventSource object's connection. It can have the values described below. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState) + */ + get readyState(): number + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + get onopen(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + set onopen(value: any | null) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + get onmessage(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + set onmessage(value: any | null) + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + get onerror(): any | null + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + set onerror(value: any | null) + static readonly CONNECTING: number + static readonly OPEN: number + static readonly CLOSED: number + static from(stream: ReadableStream): EventSource +} +interface EventSourceEventSourceInit { + withCredentials?: boolean + fetcher?: Fetcher +} +interface Container { + get running(): boolean + start(options?: ContainerStartupOptions): void + monitor(): Promise + destroy(error?: any): Promise + signal(signo: number): void + getTcpPort(port: number): Fetcher +} +interface ContainerStartupOptions { + entrypoint?: string[] + enableInternet: boolean + env?: Record +} +type AiImageClassificationInput = { + image: number[] +} +type AiImageClassificationOutput = { + score?: number + label?: string +}[] +declare abstract class BaseAiImageClassification { + inputs: AiImageClassificationInput + postProcessedOutputs: AiImageClassificationOutput +} +type AiImageToTextInput = { + image: number[] + prompt?: string + max_tokens?: number + temperature?: number + top_p?: number + top_k?: number + seed?: number + repetition_penalty?: number + frequency_penalty?: number + presence_penalty?: number + raw?: boolean + messages?: RoleScopedChatInput[] +} +type AiImageToTextOutput = { + description: string +} +declare abstract class BaseAiImageToText { + inputs: AiImageToTextInput + postProcessedOutputs: AiImageToTextOutput +} +type AiImageTextToTextInput = { + image: string + prompt?: string + max_tokens?: number + temperature?: number + ignore_eos?: boolean + top_p?: number + top_k?: number + seed?: number + repetition_penalty?: number + frequency_penalty?: number + presence_penalty?: number + raw?: boolean + messages?: RoleScopedChatInput[] +} +type AiImageTextToTextOutput = { + description: string +} +declare abstract class BaseAiImageTextToText { + inputs: AiImageTextToTextInput + postProcessedOutputs: AiImageTextToTextOutput +} +type AiObjectDetectionInput = { + image: number[] +} +type AiObjectDetectionOutput = { + score?: number + label?: string +}[] +declare abstract class BaseAiObjectDetection { + inputs: AiObjectDetectionInput + postProcessedOutputs: AiObjectDetectionOutput +} +type AiSentenceSimilarityInput = { + source: string + sentences: string[] +} +type AiSentenceSimilarityOutput = number[] +declare abstract class BaseAiSentenceSimilarity { + inputs: AiSentenceSimilarityInput + postProcessedOutputs: AiSentenceSimilarityOutput +} +type AiAutomaticSpeechRecognitionInput = { + audio: number[] +} +type AiAutomaticSpeechRecognitionOutput = { + text?: string + words?: { + word: string + start: number + end: number + }[] + vtt?: string +} +declare abstract class BaseAiAutomaticSpeechRecognition { + inputs: AiAutomaticSpeechRecognitionInput + postProcessedOutputs: AiAutomaticSpeechRecognitionOutput +} +type AiSummarizationInput = { + input_text: string + max_length?: number +} +type AiSummarizationOutput = { + summary: string +} +declare abstract class BaseAiSummarization { + inputs: AiSummarizationInput + postProcessedOutputs: AiSummarizationOutput +} +type AiTextClassificationInput = { + text: string +} +type AiTextClassificationOutput = { + score?: number + label?: string +}[] +declare abstract class BaseAiTextClassification { + inputs: AiTextClassificationInput + postProcessedOutputs: AiTextClassificationOutput +} +type AiTextEmbeddingsInput = { + text: string | string[] +} +type AiTextEmbeddingsOutput = { + shape: number[] + data: number[][] +} +declare abstract class BaseAiTextEmbeddings { + inputs: AiTextEmbeddingsInput + postProcessedOutputs: AiTextEmbeddingsOutput +} +type RoleScopedChatInput = { + role: 'user' | 'assistant' | 'system' | 'tool' | (string & NonNullable) + content: string + name?: string +} +type AiTextGenerationToolLegacyInput = { + name: string + description: string + parameters?: { + type: 'object' | (string & NonNullable) + properties: { + [key: string]: { + type: string + description?: string + } + } + required: string[] + } +} +type AiTextGenerationToolInput = { + type: 'function' | (string & NonNullable) + function: { + name: string + description: string + parameters?: { + type: 'object' | (string & NonNullable) + properties: { + [key: string]: { + type: string + description?: string + } + } + required: string[] + } + } +} +type AiTextGenerationFunctionsInput = { + name: string + code: string +} +type AiTextGenerationResponseFormat = { + type: string + json_schema?: any +} +type AiTextGenerationInput = { + prompt?: string + raw?: boolean + stream?: boolean + max_tokens?: number + temperature?: number + top_p?: number + top_k?: number + seed?: number + repetition_penalty?: number + frequency_penalty?: number + presence_penalty?: number + messages?: RoleScopedChatInput[] + response_format?: AiTextGenerationResponseFormat + tools?: + | AiTextGenerationToolInput[] + | AiTextGenerationToolLegacyInput[] + | (object & NonNullable) + functions?: AiTextGenerationFunctionsInput[] +} +type AiTextGenerationOutput = { + response?: string + tool_calls?: { + name: string + arguments: unknown + }[] +} +declare abstract class BaseAiTextGeneration { + inputs: AiTextGenerationInput + postProcessedOutputs: AiTextGenerationOutput +} +type AiTextToSpeechInput = { + prompt: string + lang?: string +} +type AiTextToSpeechOutput = + | Uint8Array + | { + audio: string + } +declare abstract class BaseAiTextToSpeech { + inputs: AiTextToSpeechInput + postProcessedOutputs: AiTextToSpeechOutput +} +type AiTextToImageInput = { + prompt: string + negative_prompt?: string + height?: number + width?: number + image?: number[] + image_b64?: string + mask?: number[] + num_steps?: number + strength?: number + guidance?: number + seed?: number +} +type AiTextToImageOutput = ReadableStream +declare abstract class BaseAiTextToImage { + inputs: AiTextToImageInput + postProcessedOutputs: AiTextToImageOutput +} +type AiTranslationInput = { + text: string + target_lang: string + source_lang?: string +} +type AiTranslationOutput = { + translated_text?: string +} +declare abstract class BaseAiTranslation { + inputs: AiTranslationInput + postProcessedOutputs: AiTranslationOutput +} +type Ai_Cf_Baai_Bge_Base_En_V1_5_Input = + | { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + }[] + } +type Ai_Cf_Baai_Bge_Base_En_V1_5_Output = + | { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' + } + | AsyncResponse +interface AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string +} +declare abstract class Base_Ai_Cf_Baai_Bge_Base_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Output +} +type Ai_Cf_Openai_Whisper_Input = + | string + | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[] + } +interface Ai_Cf_Openai_Whisper_Output { + /** + * The transcription + */ + text: string + word_count?: number + words?: { + word?: string + /** + * The second this word begins in the recording + */ + start?: number + /** + * The ending second when the word completes + */ + end?: number + }[] + vtt?: string +} +declare abstract class Base_Ai_Cf_Openai_Whisper { + inputs: Ai_Cf_Openai_Whisper_Input + postProcessedOutputs: Ai_Cf_Openai_Whisper_Output +} +type Ai_Cf_Meta_M2M100_1_2B_Input = + | { + /** + * The text to be translated + */ + text: string + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + /** + * The text to be translated + */ + text: string + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string + }[] + } +type Ai_Cf_Meta_M2M100_1_2B_Output = + | { + /** + * The translated text in the target language + */ + translated_text?: string + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Meta_M2M100_1_2B { + inputs: Ai_Cf_Meta_M2M100_1_2B_Input + postProcessedOutputs: Ai_Cf_Meta_M2M100_1_2B_Output +} +type Ai_Cf_Baai_Bge_Small_En_V1_5_Input = + | { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + }[] + } +type Ai_Cf_Baai_Bge_Small_En_V1_5_Output = + | { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Baai_Bge_Small_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Output +} +type Ai_Cf_Baai_Bge_Large_En_V1_5_Input = + | { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + } + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[] + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: 'mean' | 'cls' + }[] + } +type Ai_Cf_Baai_Bge_Large_En_V1_5_Output = + | { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Baai_Bge_Large_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Output +} +type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input = + | string + | { + /** + * The input text prompt for the model to generate a response. + */ + prompt?: string + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number + image: number[] | (string & NonNullable) + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + } +interface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output { + description?: string +} +declare abstract class Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M { + inputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input + postProcessedOutputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output +} +type Ai_Cf_Openai_Whisper_Tiny_En_Input = + | string + | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[] + } +interface Ai_Cf_Openai_Whisper_Tiny_En_Output { + /** + * The transcription + */ + text: string + word_count?: number + words?: { + word?: string + /** + * The second this word begins in the recording + */ + start?: number + /** + * The ending second when the word completes + */ + end?: number + }[] + vtt?: string +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Tiny_En { + inputs: Ai_Cf_Openai_Whisper_Tiny_En_Input + postProcessedOutputs: Ai_Cf_Openai_Whisper_Tiny_En_Output +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input { + /** + * Base64 encoded value of the audio data. + */ + audio: string + /** + * Supported tasks are 'translate' or 'transcribe'. + */ + task?: string + /** + * The language of the audio being transcribed or translated. + */ + language?: string + /** + * Preprocess the audio with a voice activity detection model. + */ + vad_filter?: boolean + /** + * A text prompt to help provide context to the model on the contents of the audio. + */ + initial_prompt?: string + /** + * The prefix it appended the the beginning of the output of the transcription and can guide the transcription result. + */ + prefix?: string +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output { + transcription_info?: { + /** + * The language of the audio being transcribed or translated. + */ + language?: string + /** + * The confidence level or probability of the detected language being accurate, represented as a decimal between 0 and 1. + */ + language_probability?: number + /** + * The total duration of the original audio file, in seconds. + */ + duration?: number + /** + * The duration of the audio after applying Voice Activity Detection (VAD) to remove silent or irrelevant sections, in seconds. + */ + duration_after_vad?: number + } + /** + * The complete transcription of the audio. + */ + text: string + /** + * The total number of words in the transcription. + */ + word_count?: number + segments?: { + /** + * The starting time of the segment within the audio, in seconds. + */ + start?: number + /** + * The ending time of the segment within the audio, in seconds. + */ + end?: number + /** + * The transcription of the segment. + */ + text?: string + /** + * The temperature used in the decoding process, controlling randomness in predictions. Lower values result in more deterministic outputs. + */ + temperature?: number + /** + * The average log probability of the predictions for the words in this segment, indicating overall confidence. + */ + avg_logprob?: number + /** + * The compression ratio of the input to the output, measuring how much the text was compressed during the transcription process. + */ + compression_ratio?: number + /** + * The probability that the segment contains no speech, represented as a decimal between 0 and 1. + */ + no_speech_prob?: number + words?: { + /** + * The individual word transcribed from the audio. + */ + word?: string + /** + * The starting time of the word within the audio, in seconds. + */ + start?: number + /** + * The ending time of the word within the audio, in seconds. + */ + end?: number + }[] + }[] + /** + * The transcription in WebVTT format, which includes timing and text information for use in subtitles. + */ + vtt?: string +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo { + inputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input + postProcessedOutputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output +} +type Ai_Cf_Baai_Bge_M3_Input = + | BGEM3InputQueryAndContexts + | BGEM3InputEmbedding + | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: (BGEM3InputQueryAndContexts1 | BGEM3InputEmbedding1)[] + } +interface BGEM3InputQueryAndContexts { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string + }[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +interface BGEM3InputEmbedding { + text: string | string[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +interface BGEM3InputQueryAndContexts1 { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string + }[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +interface BGEM3InputEmbedding1 { + text: string | string[] + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean +} +type Ai_Cf_Baai_Bge_M3_Output = + | BGEM3OuputQuery + | BGEM3OutputEmbeddingForContexts + | BGEM3OuputEmbedding + | AsyncResponse +interface BGEM3OuputQuery { + response?: { + /** + * Index of the context in the request + */ + id?: number + /** + * Score of the context under the index. + */ + score?: number + }[] +} +interface BGEM3OutputEmbeddingForContexts { + response?: number[][] + shape?: number[] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' +} +interface BGEM3OuputEmbedding { + shape?: number[] + /** + * Embeddings of the requested text values + */ + data?: number[][] + /** + * The pooling method used in the embedding process. + */ + pooling?: 'mean' | 'cls' +} +declare abstract class Base_Ai_Cf_Baai_Bge_M3 { + inputs: Ai_Cf_Baai_Bge_M3_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_M3_Output +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string + /** + * The number of diffusion steps; higher values can improve quality but take longer. + */ + steps?: number +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output { + /** + * The generated image in Base64 format. + */ + image?: string +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell { + inputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input = Prompt | Messages +interface Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + image?: number[] | (string & NonNullable) + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string +} +interface Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + image?: number[] | (string & NonNullable) + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * If true, the response will be streamed back incrementally. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = { + /** + * The generated text response from the model + */ + response?: string + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct { + inputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input = + | Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt + | Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages + | AsyncBatch +interface Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface JSONMode { + type?: 'json_object' | 'json_schema' + json_schema?: unknown +} +interface Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string + /** + * The content of the message as a string. + */ + content: string + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface AsyncBatch { + requests?: { + /** + * User-supplied reference. This field will be present in the response as well it can be used to reference the request and response. It's NOT validated to be unique. + */ + external_reference?: string + /** + * Prompt for the text generation model + */ + prompt?: string + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number + response_format?: JSONMode + }[] +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = + | { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] + } + | AsyncResponse +declare abstract class Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast { + inputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Input { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender must alternate between 'user' and 'assistant'. + */ + role: 'user' | 'assistant' + /** + * The content of the message as a string. + */ + content: string + }[] + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Dictate the output format of the generated response. + */ + response_format?: { + /** + * Set to json_object to process and output generated text as JSON. + */ + type?: string + } +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Output { + response?: + | string + | { + /** + * Whether the conversation is safe or not. + */ + safe?: boolean + /** + * A list of what hazard categories predicted for the conversation, if the conversation is deemed unsafe. + */ + categories?: string[] + } + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } +} +declare abstract class Base_Ai_Cf_Meta_Llama_Guard_3_8B { + inputs: Ai_Cf_Meta_Llama_Guard_3_8B_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_Guard_3_8B_Output +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Input { + /** + * A query you wish to perform against the provided contexts. + */ + query: string + /** + * Number of returned results starting with the best score. + */ + top_k?: number + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string + }[] +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Output { + response?: { + /** + * Index of the context in the request + */ + id?: number + /** + * Score of the context under the index. + */ + score?: number + }[] +} +declare abstract class Base_Ai_Cf_Baai_Bge_Reranker_Base { + inputs: Ai_Cf_Baai_Bge_Reranker_Base_Input + postProcessedOutputs: Ai_Cf_Baai_Bge_Reranker_Base_Output +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input = + | Qwen2_5_Coder_32B_Instruct_Prompt + | Qwen2_5_Coder_32B_Instruct_Messages +interface Qwen2_5_Coder_32B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Qwen2_5_Coder_32B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string + /** + * The content of the message as a string. + */ + content: string + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct { + inputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input + postProcessedOutputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output +} +type Ai_Cf_Qwen_Qwq_32B_Input = Qwen_Qwq_32B_Prompt | Qwen_Qwq_32B_Messages +interface Qwen_Qwq_32B_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Qwen_Qwq_32B_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Qwen_Qwq_32B_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Qwen_Qwq_32B { + inputs: Ai_Cf_Qwen_Qwq_32B_Input + postProcessedOutputs: Ai_Cf_Qwen_Qwq_32B_Output +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input = + | Mistral_Small_3_1_24B_Instruct_Prompt + | Mistral_Small_3_1_24B_Instruct_Messages +interface Mistral_Small_3_1_24B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Mistral_Small_3_1_24B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct { + inputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input + postProcessedOutputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output +} +type Ai_Cf_Google_Gemma_3_12B_It_Input = + | Google_Gemma_3_12B_It_Prompt + | Google_Gemma_3_12B_It_Messages +interface Google_Gemma_3_12B_It_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Google_Gemma_3_12B_It_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Google_Gemma_3_12B_It_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + /** + * The name of the tool to be called + */ + name?: string + }[] +} +declare abstract class Base_Ai_Cf_Google_Gemma_3_12B_It { + inputs: Ai_Cf_Google_Gemma_3_12B_It_Input + postProcessedOutputs: Ai_Cf_Google_Gemma_3_12B_It_Output +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = + | Ai_Cf_Meta_Llama_4_Prompt + | Ai_Cf_Meta_Llama_4_Messages +interface Ai_Cf_Meta_Llama_4_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object + response_format?: JSONMode + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +interface Ai_Cf_Meta_Llama_4_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string + content?: + | string + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + }[] + | { + /** + * Type of the content provided + */ + type?: string + text?: string + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string + } + } + }[] + functions?: { + name: string + code: string + }[] + /** + * A list of tools available for the assistant to use. + */ + tools?: ( + | { + /** + * The name of the tool. More descriptive the better. + */ + name: string + /** + * A brief description of what the tool does. + */ + description: string + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string + /** + * A brief description of what the function does. + */ + description: string + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string + /** + * List of required parameter names. + */ + required?: string[] + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string + /** + * A description of the expected parameter. + */ + description: string + } + } + } + } + } + )[] + response_format?: JSONMode + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number + /** + * Random seed for reproducibility of the generation. + */ + seed?: number + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number + /** + * Total number of tokens in output + */ + completion_tokens?: number + /** + * Total number of input and output tokens + */ + total_tokens?: number + } + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The tool call id. + */ + id?: string + /** + * Specifies the type of tool (e.g., 'function'). + */ + type?: string + /** + * Details of the function tool. + */ + function?: { + /** + * The name of the tool to be called + */ + name?: string + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object + } + }[] +} +declare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct { + inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input + postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output +} +interface AiModels { + '@cf/huggingface/distilbert-sst-2-int8': BaseAiTextClassification + '@cf/stabilityai/stable-diffusion-xl-base-1.0': BaseAiTextToImage + '@cf/runwayml/stable-diffusion-v1-5-inpainting': BaseAiTextToImage + '@cf/runwayml/stable-diffusion-v1-5-img2img': BaseAiTextToImage + '@cf/lykon/dreamshaper-8-lcm': BaseAiTextToImage + '@cf/bytedance/stable-diffusion-xl-lightning': BaseAiTextToImage + '@cf/myshell-ai/melotts': BaseAiTextToSpeech + '@cf/microsoft/resnet-50': BaseAiImageClassification + '@cf/facebook/detr-resnet-50': BaseAiObjectDetection + '@cf/meta/llama-2-7b-chat-int8': BaseAiTextGeneration + '@cf/mistral/mistral-7b-instruct-v0.1': BaseAiTextGeneration + '@cf/meta/llama-2-7b-chat-fp16': BaseAiTextGeneration + '@hf/thebloke/llama-2-13b-chat-awq': BaseAiTextGeneration + '@hf/thebloke/mistral-7b-instruct-v0.1-awq': BaseAiTextGeneration + '@hf/thebloke/zephyr-7b-beta-awq': BaseAiTextGeneration + '@hf/thebloke/openhermes-2.5-mistral-7b-awq': BaseAiTextGeneration + '@hf/thebloke/neural-chat-7b-v3-1-awq': BaseAiTextGeneration + '@hf/thebloke/llamaguard-7b-awq': BaseAiTextGeneration + '@hf/thebloke/deepseek-coder-6.7b-base-awq': BaseAiTextGeneration + '@hf/thebloke/deepseek-coder-6.7b-instruct-awq': BaseAiTextGeneration + '@cf/deepseek-ai/deepseek-math-7b-instruct': BaseAiTextGeneration + '@cf/defog/sqlcoder-7b-2': BaseAiTextGeneration + '@cf/openchat/openchat-3.5-0106': BaseAiTextGeneration + '@cf/tiiuae/falcon-7b-instruct': BaseAiTextGeneration + '@cf/thebloke/discolm-german-7b-v1-awq': BaseAiTextGeneration + '@cf/qwen/qwen1.5-0.5b-chat': BaseAiTextGeneration + '@cf/qwen/qwen1.5-7b-chat-awq': BaseAiTextGeneration + '@cf/qwen/qwen1.5-14b-chat-awq': BaseAiTextGeneration + '@cf/tinyllama/tinyllama-1.1b-chat-v1.0': BaseAiTextGeneration + '@cf/microsoft/phi-2': BaseAiTextGeneration + '@cf/qwen/qwen1.5-1.8b-chat': BaseAiTextGeneration + '@cf/mistral/mistral-7b-instruct-v0.2-lora': BaseAiTextGeneration + '@hf/nousresearch/hermes-2-pro-mistral-7b': BaseAiTextGeneration + '@hf/nexusflow/starling-lm-7b-beta': BaseAiTextGeneration + '@hf/google/gemma-7b-it': BaseAiTextGeneration + '@cf/meta-llama/llama-2-7b-chat-hf-lora': BaseAiTextGeneration + '@cf/google/gemma-2b-it-lora': BaseAiTextGeneration + '@cf/google/gemma-7b-it-lora': BaseAiTextGeneration + '@hf/mistral/mistral-7b-instruct-v0.2': BaseAiTextGeneration + '@cf/meta/llama-3-8b-instruct': BaseAiTextGeneration + '@cf/fblgit/una-cybertron-7b-v2-bf16': BaseAiTextGeneration + '@cf/meta/llama-3-8b-instruct-awq': BaseAiTextGeneration + '@hf/meta-llama/meta-llama-3-8b-instruct': BaseAiTextGeneration + '@cf/meta/llama-3.1-8b-instruct': BaseAiTextGeneration + '@cf/meta/llama-3.1-8b-instruct-fp8': BaseAiTextGeneration + '@cf/meta/llama-3.1-8b-instruct-awq': BaseAiTextGeneration + '@cf/meta/llama-3.2-3b-instruct': BaseAiTextGeneration + '@cf/meta/llama-3.2-1b-instruct': BaseAiTextGeneration + '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b': BaseAiTextGeneration + '@cf/facebook/bart-large-cnn': BaseAiSummarization + '@cf/llava-hf/llava-1.5-7b-hf': BaseAiImageToText + '@cf/baai/bge-base-en-v1.5': Base_Ai_Cf_Baai_Bge_Base_En_V1_5 + '@cf/openai/whisper': Base_Ai_Cf_Openai_Whisper + '@cf/meta/m2m100-1.2b': Base_Ai_Cf_Meta_M2M100_1_2B + '@cf/baai/bge-small-en-v1.5': Base_Ai_Cf_Baai_Bge_Small_En_V1_5 + '@cf/baai/bge-large-en-v1.5': Base_Ai_Cf_Baai_Bge_Large_En_V1_5 + '@cf/unum/uform-gen2-qwen-500m': Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M + '@cf/openai/whisper-tiny-en': Base_Ai_Cf_Openai_Whisper_Tiny_En + '@cf/openai/whisper-large-v3-turbo': Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo + '@cf/baai/bge-m3': Base_Ai_Cf_Baai_Bge_M3 + '@cf/black-forest-labs/flux-1-schnell': Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell + '@cf/meta/llama-3.2-11b-vision-instruct': Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct + '@cf/meta/llama-3.3-70b-instruct-fp8-fast': Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast + '@cf/meta/llama-guard-3-8b': Base_Ai_Cf_Meta_Llama_Guard_3_8B + '@cf/baai/bge-reranker-base': Base_Ai_Cf_Baai_Bge_Reranker_Base + '@cf/qwen/qwen2.5-coder-32b-instruct': Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct + '@cf/qwen/qwq-32b': Base_Ai_Cf_Qwen_Qwq_32B + '@cf/mistralai/mistral-small-3.1-24b-instruct': Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct + '@cf/google/gemma-3-12b-it': Base_Ai_Cf_Google_Gemma_3_12B_It + '@cf/meta/llama-4-scout-17b-16e-instruct': Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct +} +type AiOptions = { + /** + * Send requests as an asynchronous batch job, only works for supported models + * https://developers.cloudflare.com/workers-ai/features/batch-api + */ + queueRequest?: boolean + gateway?: GatewayOptions + returnRawResponse?: boolean + prefix?: string + extraHeaders?: object +} +type ConversionResponse = { + name: string + mimeType: string + format: 'markdown' + tokens: number + data: string +} +type AiModelsSearchParams = { + author?: string + hide_experimental?: boolean + page?: number + per_page?: number + search?: string + source?: number + task?: string +} +type AiModelsSearchObject = { + id: string + source: number + name: string + description: string + task: { + id: string + name: string + description: string + } + tags: string[] + properties: { + property_id: string + value: string + }[] +} +interface InferenceUpstreamError extends Error {} +interface AiInternalError extends Error {} +type AiModelListType = Record +declare abstract class Ai { + aiGatewayLogId: string | null + gateway(gatewayId: string): AiGateway + autorag(autoragId: string): AutoRAG + run< + Name extends keyof AiModelList, + Options extends AiOptions, + InputOptions extends AiModelList[Name]['inputs'], + >( + model: Name, + inputs: InputOptions, + options?: Options + ): Promise< + Options extends { + returnRawResponse: true + } + ? Response + : InputOptions extends { + stream: true + } + ? ReadableStream + : AiModelList[Name]['postProcessedOutputs'] + > + models(params?: AiModelsSearchParams): Promise + toMarkdown( + files: { + name: string + blob: Blob + }[], + options?: { + gateway?: GatewayOptions + extraHeaders?: object + } + ): Promise + toMarkdown( + files: { + name: string + blob: Blob + }, + options?: { + gateway?: GatewayOptions + extraHeaders?: object + } + ): Promise +} +type GatewayRetries = { + maxAttempts?: 1 | 2 | 3 | 4 | 5 + retryDelayMs?: number + backoff?: 'constant' | 'linear' | 'exponential' +} +type GatewayOptions = { + id: string + cacheKey?: string + cacheTtl?: number + skipCache?: boolean + metadata?: Record + collectLog?: boolean + eventId?: string + requestTimeoutMs?: number + retries?: GatewayRetries +} +type AiGatewayPatchLog = { + score?: number | null + feedback?: -1 | 1 | null + metadata?: Record | null +} +type AiGatewayLog = { + id: string + provider: string + model: string + model_type?: string + path: string + duration: number + request_type?: string + request_content_type?: string + status_code: number + response_content_type?: string + success: boolean + cached: boolean + tokens_in?: number + tokens_out?: number + metadata?: Record + step?: number + cost?: number + custom_cost?: boolean + request_size: number + request_head?: string + request_head_complete: boolean + response_size: number + response_head?: string + response_head_complete: boolean + created_at: Date +} +type AIGatewayProviders = + | 'workers-ai' + | 'anthropic' + | 'aws-bedrock' + | 'azure-openai' + | 'google-vertex-ai' + | 'huggingface' + | 'openai' + | 'perplexity-ai' + | 'replicate' + | 'groq' + | 'cohere' + | 'google-ai-studio' + | 'mistral' + | 'grok' + | 'openrouter' + | 'deepseek' + | 'cerebras' + | 'cartesia' + | 'elevenlabs' + | 'adobe-firefly' +type AIGatewayHeaders = { + 'cf-aig-metadata': Record | string + 'cf-aig-custom-cost': + | { + per_token_in?: number + per_token_out?: number + } + | { + total_cost?: number + } + | string + 'cf-aig-cache-ttl': number | string + 'cf-aig-skip-cache': boolean | string + 'cf-aig-cache-key': string + 'cf-aig-event-id': string + 'cf-aig-request-timeout': number | string + 'cf-aig-max-attempts': number | string + 'cf-aig-retry-delay': number | string + 'cf-aig-backoff': string + 'cf-aig-collect-log': boolean | string + Authorization: string + 'Content-Type': string + [key: string]: string | number | boolean | object +} +type AIGatewayUniversalRequest = { + provider: AIGatewayProviders | string // eslint-disable-line + endpoint: string + headers: Partial + query: unknown +} +interface AiGatewayInternalError extends Error {} +interface AiGatewayLogNotFound extends Error {} +declare abstract class AiGateway { + patchLog(logId: string, data: AiGatewayPatchLog): Promise + getLog(logId: string): Promise + run( + data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], + options?: { + gateway?: GatewayOptions + extraHeaders?: object + } + ): Promise + getUrl(provider?: AIGatewayProviders | string): Promise // eslint-disable-line +} +interface AutoRAGInternalError extends Error {} +interface AutoRAGNotFoundError extends Error {} +interface AutoRAGUnauthorizedError extends Error {} +interface AutoRAGNameNotSetError extends Error {} +type ComparisonFilter = { + key: string + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' + value: string | number | boolean +} +type CompoundFilter = { + type: 'and' | 'or' + filters: ComparisonFilter[] +} +type AutoRagSearchRequest = { + query: string + filters?: CompoundFilter | ComparisonFilter + max_num_results?: number + ranking_options?: { + ranker?: string + score_threshold?: number + } + rewrite_query?: boolean +} +type AutoRagAiSearchRequest = AutoRagSearchRequest & { + stream?: boolean +} +type AutoRagAiSearchRequestStreaming = Omit & { + stream: true +} +type AutoRagSearchResponse = { + object: 'vector_store.search_results.page' + search_query: string + data: { + file_id: string + filename: string + score: number + attributes: Record + content: { + type: 'text' + text: string + }[] + }[] + has_more: boolean + next_page: string | null +} +type AutoRagListResponse = { + id: string + enable: boolean + type: string + source: string + vectorize_name: string + paused: boolean + status: string +}[] +type AutoRagAiSearchResponse = AutoRagSearchResponse & { + response: string +} +declare abstract class AutoRAG { + list(): Promise + search(params: AutoRagSearchRequest): Promise + aiSearch(params: AutoRagAiSearchRequestStreaming): Promise + aiSearch(params: AutoRagAiSearchRequest): Promise + aiSearch(params: AutoRagAiSearchRequest): Promise +} +interface BasicImageTransformations { + /** + * Maximum width in image pixels. The value must be an integer. + */ + width?: number + /** + * Maximum height in image pixels. The value must be an integer. + */ + height?: number + /** + * Resizing mode as a string. It affects interpretation of width and height + * options: + * - scale-down: Similar to contain, but the image is never enlarged. If + * the image is larger than given width or height, it will be resized. + * Otherwise its original size will be kept. + * - contain: Resizes to maximum size that fits within the given width and + * height. If only a single dimension is given (e.g. only width), the + * image will be shrunk or enlarged to exactly match that dimension. + * Aspect ratio is always preserved. + * - cover: Resizes (shrinks or enlarges) to fill the entire area of width + * and height. If the image has an aspect ratio different from the ratio + * of width and height, it will be cropped to fit. + * - crop: The image will be shrunk and cropped to fit within the area + * specified by width and height. The image will not be enlarged. For images + * smaller than the given dimensions it's the same as scale-down. For + * images larger than the given dimensions, it's the same as cover. + * See also trim. + * - pad: Resizes to the maximum size that fits within the given width and + * height, and then fills the remaining area with a background color + * (white by default). Use of this mode is not recommended, as the same + * effect can be more efficiently achieved with the contain mode and the + * CSS object-fit: contain property. + * - squeeze: Stretches and deforms to the width and height given, even if it + * breaks aspect ratio + */ + fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad' | 'squeeze' + /** + * When cropping with fit: "cover", this defines the side or point that should + * be left uncropped. The value is either a string + * "left", "right", "top", "bottom", "auto", or "center" (the default), + * or an object {x, y} containing focal point coordinates in the original + * image expressed as fractions ranging from 0.0 (top or left) to 1.0 + * (bottom or right), 0.5 being the center. {fit: "cover", gravity: "top"} will + * crop bottom or left and right sides as necessary, but won’t crop anything + * from the top. {fit: "cover", gravity: {x:0.5, y:0.2}} will crop each side to + * preserve as much as possible around a point at 20% of the height of the + * source image. + */ + gravity?: + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'center' + | 'auto' + | 'entropy' + | BasicImageTransformationsGravityCoordinates + /** + * Background color to add underneath the image. Applies only to images with + * transparency (such as PNG). Accepts any CSS color (#RRGGBB, rgba(…), + * hsl(…), etc.) + */ + background?: string + /** + * Number of degrees (90, 180, 270) to rotate the image by. width and height + * options refer to axes after rotation. + */ + rotate?: 0 | 90 | 180 | 270 | 360 +} +interface BasicImageTransformationsGravityCoordinates { + x?: number + y?: number + mode?: 'remainder' | 'box-center' +} +/** + * In addition to the properties you can set in the RequestInit dict + * that you pass as an argument to the Request constructor, you can + * set certain properties of a `cf` object to control how Cloudflare + * features are applied to that new Request. + * + * Note: Currently, these properties cannot be tested in the + * playground. + */ +interface RequestInitCfProperties extends Record { + cacheEverything?: boolean + /** + * A request's cache key is what determines if two requests are + * "the same" for caching purposes. If a request has the same cache key + * as some previous request, then we can serve the same cached response for + * both. (e.g. 'some-key') + * + * Only available for Enterprise customers. + */ + cacheKey?: string + /** + * This allows you to append additional Cache-Tag response headers + * to the origin response without modifications to the origin server. + * This will allow for greater control over the Purge by Cache Tag feature + * utilizing changes only in the Workers process. + * + * Only available for Enterprise customers. + */ + cacheTags?: string[] + /** + * Force response to be cached for a given number of seconds. (e.g. 300) + */ + cacheTtl?: number + /** + * Force response to be cached for a given number of seconds based on the Origin status code. + * (e.g. { '200-299': 86400, '404': 1, '500-599': 0 }) + */ + cacheTtlByStatus?: Record + scrapeShield?: boolean + apps?: boolean + image?: RequestInitCfPropertiesImage + minify?: RequestInitCfPropertiesImageMinify + mirage?: boolean + polish?: 'lossy' | 'lossless' | 'off' + r2?: RequestInitCfPropertiesR2 + /** + * Redirects the request to an alternate origin server. You can use this, + * for example, to implement load balancing across several origins. + * (e.g.us-east.example.com) + * + * Note - For security reasons, the hostname set in resolveOverride must + * be proxied on the same Cloudflare zone of the incoming request. + * Otherwise, the setting is ignored. CNAME hosts are allowed, so to + * resolve to a host under a different domain or a DNS only domain first + * declare a CNAME record within your own zone’s DNS mapping to the + * external hostname, set proxy on Cloudflare, then set resolveOverride + * to point to that CNAME record. + */ + resolveOverride?: string +} +interface RequestInitCfPropertiesImageDraw extends BasicImageTransformations { + /** + * Absolute URL of the image file to use for the drawing. It can be any of + * the supported file formats. For drawing of watermarks or non-rectangular + * overlays we recommend using PNG or WebP images. + */ + url: string + /** + * Floating-point number between 0 (transparent) and 1 (opaque). + * For example, opacity: 0.5 makes overlay semitransparent. + */ + opacity?: number + /** + * - If set to true, the overlay image will be tiled to cover the entire + * area. This is useful for stock-photo-like watermarks. + * - If set to "x", the overlay image will be tiled horizontally only + * (form a line). + * - If set to "y", the overlay image will be tiled vertically only + * (form a line). + */ + repeat?: true | 'x' | 'y' + /** + * Position of the overlay image relative to a given edge. Each property is + * an offset in pixels. 0 aligns exactly to the edge. For example, left: 10 + * positions left side of the overlay 10 pixels from the left edge of the + * image it's drawn over. bottom: 0 aligns bottom of the overlay with bottom + * of the background image. + * + * Setting both left & right, or both top & bottom is an error. + * + * If no position is specified, the image will be centered. + */ + top?: number + left?: number + bottom?: number + right?: number +} +interface RequestInitCfPropertiesImage extends BasicImageTransformations { + /** + * Device Pixel Ratio. Default 1. Multiplier for width/height that makes it + * easier to specify higher-DPI sizes in . + */ + dpr?: number + /** + * Allows you to trim your image. Takes dpr into account and is performed before + * resizing or rotation. + * + * It can be used as: + * - left, top, right, bottom - it will specify the number of pixels to cut + * off each side + * - width, height - the width/height you'd like to end up with - can be used + * in combination with the properties above + * - border - this will automatically trim the surroundings of an image based on + * it's color. It consists of three properties: + * - color: rgb or hex representation of the color you wish to trim (todo: verify the rgba bit) + * - tolerance: difference from color to treat as color + * - keep: the number of pixels of border to keep + */ + trim?: + | 'border' + | { + top?: number + bottom?: number + left?: number + right?: number + width?: number + height?: number + border?: + | boolean + | { + color?: string + tolerance?: number + keep?: number + } + } + /** + * Quality setting from 1-100 (useful values are in 60-90 range). Lower values + * make images look worse, but load faster. The default is 85. It applies only + * to JPEG and WebP images. It doesn’t have any effect on PNG. + */ + quality?: number | 'low' | 'medium-low' | 'medium-high' | 'high' + /** + * Output format to generate. It can be: + * - avif: generate images in AVIF format. + * - webp: generate images in Google WebP format. Set quality to 100 to get + * the WebP-lossless format. + * - json: instead of generating an image, outputs information about the + * image, in JSON format. The JSON object will contain image size + * (before and after resizing), source image’s MIME type, file size, etc. + * - jpeg: generate images in JPEG format. + * - png: generate images in PNG format. + */ + format?: 'avif' | 'webp' | 'json' | 'jpeg' | 'png' | 'baseline-jpeg' | 'png-force' | 'svg' + /** + * Whether to preserve animation frames from input files. Default is true. + * Setting it to false reduces animations to still images. This setting is + * recommended when enlarging images or processing arbitrary user content, + * because large GIF animations can weigh tens or even hundreds of megabytes. + * It is also useful to set anim:false when using format:"json" to get the + * response quicker without the number of frames. + */ + anim?: boolean + /** + * What EXIF data should be preserved in the output image. Note that EXIF + * rotation and embedded color profiles are always applied ("baked in" into + * the image), and aren't affected by this option. Note that if the Polish + * feature is enabled, all metadata may have been removed already and this + * option may have no effect. + * - keep: Preserve most of EXIF metadata, including GPS location if there's + * any. + * - copyright: Only keep the copyright tag, and discard everything else. + * This is the default behavior for JPEG files. + * - none: Discard all invisible EXIF metadata. Currently WebP and PNG + * output formats always discard metadata. + */ + metadata?: 'keep' | 'copyright' | 'none' + /** + * Strength of sharpening filter to apply to the image. Floating-point + * number between 0 (no sharpening, default) and 10 (maximum). 1.0 is a + * recommended value for downscaled images. + */ + sharpen?: number + /** + * Radius of a blur filter (approximate gaussian). Maximum supported radius + * is 250. + */ + blur?: number + /** + * Overlays are drawn in the order they appear in the array (last array + * entry is the topmost layer). + */ + draw?: RequestInitCfPropertiesImageDraw[] + /** + * Fetching image from authenticated origin. Setting this property will + * pass authentication headers (Authorization, Cookie, etc.) through to + * the origin. + */ + 'origin-auth'?: 'share-publicly' + /** + * Adds a border around the image. The border is added after resizing. Border + * width takes dpr into account, and can be specified either using a single + * width property, or individually for each side. + */ + border?: + | { + color: string + width: number + } + | { + color: string + top: number + right: number + bottom: number + left: number + } + /** + * Increase brightness by a factor. A value of 1.0 equals no change, a value + * of 0.5 equals half brightness, and a value of 2.0 equals twice as bright. + * 0 is ignored. + */ + brightness?: number + /** + * Increase contrast by a factor. A value of 1.0 equals no change, a value of + * 0.5 equals low contrast, and a value of 2.0 equals high contrast. 0 is + * ignored. + */ + contrast?: number + /** + * Increase exposure by a factor. A value of 1.0 equals no change, a value of + * 0.5 darkens the image, and a value of 2.0 lightens the image. 0 is ignored. + */ + gamma?: number + /** + * Increase contrast by a factor. A value of 1.0 equals no change, a value of + * 0.5 equals low contrast, and a value of 2.0 equals high contrast. 0 is + * ignored. + */ + saturation?: number + /** + * Flips the images horizontally, vertically, or both. Flipping is applied before + * rotation, so if you apply flip=h,rotate=90 then the image will be flipped + * horizontally, then rotated by 90 degrees. + */ + flip?: 'h' | 'v' | 'hv' + /** + * Slightly reduces latency on a cache miss by selecting a + * quickest-to-compress file format, at a cost of increased file size and + * lower image quality. It will usually override the format option and choose + * JPEG over WebP or AVIF. We do not recommend using this option, except in + * unusual circumstances like resizing uncacheable dynamically-generated + * images. + */ + compression?: 'fast' +} +interface RequestInitCfPropertiesImageMinify { + javascript?: boolean + css?: boolean + html?: boolean +} +interface RequestInitCfPropertiesR2 { + /** + * Colo id of bucket that an object is stored in + */ + bucketColoId?: number +} +/** + * Request metadata provided by Cloudflare's edge. + */ +type IncomingRequestCfProperties = IncomingRequestCfPropertiesBase & + IncomingRequestCfPropertiesBotManagementEnterprise & + IncomingRequestCfPropertiesCloudflareForSaaSEnterprise & + IncomingRequestCfPropertiesGeographicInformation & + IncomingRequestCfPropertiesCloudflareAccessOrApiShield +interface IncomingRequestCfPropertiesBase extends Record { + /** + * [ASN](https://www.iana.org/assignments/as-numbers/as-numbers.xhtml) of the incoming request. + * + * @example 395747 + */ + asn: number + /** + * The organization which owns the ASN of the incoming request. + * + * @example "Google Cloud" + */ + asOrganization: string + /** + * The original value of the `Accept-Encoding` header if Cloudflare modified it. + * + * @example "gzip, deflate, br" + */ + clientAcceptEncoding?: string + /** + * The number of milliseconds it took for the request to reach your worker. + * + * @example 22 + */ + clientTcpRtt?: number + /** + * The three-letter [IATA](https://en.wikipedia.org/wiki/IATA_airport_code) + * airport code of the data center that the request hit. + * + * @example "DFW" + */ + colo: string + /** + * Represents the upstream's response to a + * [TCP `keepalive` message](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) + * from cloudflare. + * + * For workers with no upstream, this will always be `1`. + * + * @example 3 + */ + edgeRequestKeepAliveStatus: IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus + /** + * The HTTP Protocol the request used. + * + * @example "HTTP/2" + */ + httpProtocol: string + /** + * The browser-requested prioritization information in the request object. + * + * If no information was set, defaults to the empty string `""` + * + * @example "weight=192;exclusive=0;group=3;group-weight=127" + * @default "" + */ + requestPriority: string + /** + * The TLS version of the connection to Cloudflare. + * In requests served over plaintext (without TLS), this property is the empty string `""`. + * + * @example "TLSv1.3" + */ + tlsVersion: string + /** + * The cipher for the connection to Cloudflare. + * In requests served over plaintext (without TLS), this property is the empty string `""`. + * + * @example "AEAD-AES128-GCM-SHA256" + */ + tlsCipher: string + /** + * Metadata containing the [`HELLO`](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2) and [`FINISHED`](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9) messages from this request's TLS handshake. + * + * If the incoming request was served over plaintext (without TLS) this field is undefined. + */ + tlsExportedAuthenticator?: IncomingRequestCfPropertiesExportedAuthenticatorMetadata +} +interface IncomingRequestCfPropertiesBotManagementBase { + /** + * Cloudflare’s [level of certainty](https://developers.cloudflare.com/bots/concepts/bot-score/) that a request comes from a bot, + * represented as an integer percentage between `1` (almost certainly a bot) and `99` (almost certainly human). + * + * @example 54 + */ + score: number + /** + * A boolean value that is true if the request comes from a good bot, like Google or Bing. + * Most customers choose to allow this traffic. For more details, see [Traffic from known bots](https://developers.cloudflare.com/firewall/known-issues-and-faq/#how-does-firewall-rules-handle-traffic-from-known-bots). + */ + verifiedBot: boolean + /** + * A boolean value that is true if the request originates from a + * Cloudflare-verified proxy service. + */ + corporateProxy: boolean + /** + * A boolean value that's true if the request matches [file extensions](https://developers.cloudflare.com/bots/reference/static-resources/) for many types of static resources. + */ + staticResource: boolean + /** + * List of IDs that correlate to the Bot Management heuristic detections made on a request (you can have multiple heuristic detections on the same request). + */ + detectionIds: number[] +} +interface IncomingRequestCfPropertiesBotManagement { + /** + * Results of Cloudflare's Bot Management analysis + */ + botManagement: IncomingRequestCfPropertiesBotManagementBase + /** + * Duplicate of `botManagement.score`. + * + * @deprecated + */ + clientTrustScore: number +} +interface IncomingRequestCfPropertiesBotManagementEnterprise extends IncomingRequestCfPropertiesBotManagement { + /** + * Results of Cloudflare's Bot Management analysis + */ + botManagement: IncomingRequestCfPropertiesBotManagementBase & { + /** + * A [JA3 Fingerprint](https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/) to help profile specific SSL/TLS clients + * across different destination IPs, Ports, and X509 certificates. + */ + ja3Hash: string + } +} +interface IncomingRequestCfPropertiesCloudflareForSaaSEnterprise { + /** + * Custom metadata set per-host in [Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/). + * + * This field is only present if you have Cloudflare for SaaS enabled on your account + * and you have followed the [required steps to enable it]((https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/domain-support/custom-metadata/)). + */ + hostMetadata: HostMetadata +} +interface IncomingRequestCfPropertiesCloudflareAccessOrApiShield { + /** + * Information about the client certificate presented to Cloudflare. + * + * This is populated when the incoming request is served over TLS using + * either Cloudflare Access or API Shield (mTLS) + * and the presented SSL certificate has a valid + * [Certificate Serial Number](https://ldapwiki.com/wiki/Certificate%20Serial%20Number) + * (i.e., not `null` or `""`). + * + * Otherwise, a set of placeholder values are used. + * + * The property `certPresented` will be set to `"1"` when + * the object is populated (i.e. the above conditions were met). + */ + tlsClientAuth: + | IncomingRequestCfPropertiesTLSClientAuth + | IncomingRequestCfPropertiesTLSClientAuthPlaceholder +} +/** + * Metadata about the request's TLS handshake + */ +interface IncomingRequestCfPropertiesExportedAuthenticatorMetadata { + /** + * The client's [`HELLO` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2), encoded in hexadecimal + * + * @example "44372ba35fa1270921d318f34c12f155dc87b682cf36a790cfaa3ba8737a1b5d" + */ + clientHandshake: string + /** + * The server's [`HELLO` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.1.2), encoded in hexadecimal + * + * @example "44372ba35fa1270921d318f34c12f155dc87b682cf36a790cfaa3ba8737a1b5d" + */ + serverHandshake: string + /** + * The client's [`FINISHED` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9), encoded in hexadecimal + * + * @example "084ee802fe1348f688220e2a6040a05b2199a761f33cf753abb1b006792d3f8b" + */ + clientFinished: string + /** + * The server's [`FINISHED` message](https://www.rfc-editor.org/rfc/rfc5246#section-7.4.9), encoded in hexadecimal + * + * @example "084ee802fe1348f688220e2a6040a05b2199a761f33cf753abb1b006792d3f8b" + */ + serverFinished: string +} +/** + * Geographic data about the request's origin. + */ +interface IncomingRequestCfPropertiesGeographicInformation { + /** + * The [ISO 3166-1 Alpha 2](https://www.iso.org/iso-3166-country-codes.html) country code the request originated from. + * + * If your worker is [configured to accept TOR connections](https://support.cloudflare.com/hc/en-us/articles/203306930-Understanding-Cloudflare-Tor-support-and-Onion-Routing), this may also be `"T1"`, indicating a request that originated over TOR. + * + * If Cloudflare is unable to determine where the request originated this property is omitted. + * + * The country code `"T1"` is used for requests originating on TOR. + * + * @example "GB" + */ + country?: Iso3166Alpha2Code | 'T1' + /** + * If present, this property indicates that the request originated in the EU + * + * @example "1" + */ + isEUCountry?: '1' + /** + * A two-letter code indicating the continent the request originated from. + * + * @example "AN" + */ + continent?: ContinentCode + /** + * The city the request originated from + * + * @example "Austin" + */ + city?: string + /** + * Postal code of the incoming request + * + * @example "78701" + */ + postalCode?: string + /** + * Latitude of the incoming request + * + * @example "30.27130" + */ + latitude?: string + /** + * Longitude of the incoming request + * + * @example "-97.74260" + */ + longitude?: string + /** + * Timezone of the incoming request + * + * @example "America/Chicago" + */ + timezone?: string + /** + * If known, the ISO 3166-2 name for the first level region associated with + * the IP address of the incoming request + * + * @example "Texas" + */ + region?: string + /** + * If known, the ISO 3166-2 code for the first-level region associated with + * the IP address of the incoming request + * + * @example "TX" + */ + regionCode?: string + /** + * Metro code (DMA) of the incoming request + * + * @example "635" + */ + metroCode?: string +} +/** Data about the incoming request's TLS certificate */ +interface IncomingRequestCfPropertiesTLSClientAuth { + /** Always `"1"`, indicating that the certificate was presented */ + certPresented: '1' + /** + * Result of certificate verification. + * + * @example "FAILED:self signed certificate" + */ + certVerified: Exclude + /** The presented certificate's revokation status. + * + * - A value of `"1"` indicates the certificate has been revoked + * - A value of `"0"` indicates the certificate has not been revoked + */ + certRevoked: '1' | '0' + /** + * The certificate issuer's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) + * + * @example "CN=cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certIssuerDN: string + /** + * The certificate subject's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) + * + * @example "CN=*.cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certSubjectDN: string + /** + * The certificate issuer's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) ([RFC 2253](https://www.rfc-editor.org/rfc/rfc2253.html) formatted) + * + * @example "CN=cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certIssuerDNRFC2253: string + /** + * The certificate subject's [distinguished name](https://knowledge.digicert.com/generalinformation/INFO1745.html) ([RFC 2253](https://www.rfc-editor.org/rfc/rfc2253.html) formatted) + * + * @example "CN=*.cloudflareaccess.com, C=US, ST=Texas, L=Austin, O=Cloudflare" + */ + certSubjectDNRFC2253: string + /** The certificate issuer's distinguished name (legacy policies) */ + certIssuerDNLegacy: string + /** The certificate subject's distinguished name (legacy policies) */ + certSubjectDNLegacy: string + /** + * The certificate's serial number + * + * @example "00936EACBE07F201DF" + */ + certSerial: string + /** + * The certificate issuer's serial number + * + * @example "2489002934BDFEA34" + */ + certIssuerSerial: string + /** + * The certificate's Subject Key Identifier + * + * @example "BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4" + */ + certSKI: string + /** + * The certificate issuer's Subject Key Identifier + * + * @example "BB:AF:7E:02:3D:FA:A6:F1:3C:84:8E:AD:EE:38:98:EC:D9:32:32:D4" + */ + certIssuerSKI: string + /** + * The certificate's SHA-1 fingerprint + * + * @example "6b9109f323999e52259cda7373ff0b4d26bd232e" + */ + certFingerprintSHA1: string + /** + * The certificate's SHA-256 fingerprint + * + * @example "acf77cf37b4156a2708e34c4eb755f9b5dbbe5ebb55adfec8f11493438d19e6ad3f157f81fa3b98278453d5652b0c1fd1d71e5695ae4d709803a4d3f39de9dea" + */ + certFingerprintSHA256: string + /** + * The effective starting date of the certificate + * + * @example "Dec 22 19:39:00 2018 GMT" + */ + certNotBefore: string + /** + * The effective expiration date of the certificate + * + * @example "Dec 22 19:39:00 2018 GMT" + */ + certNotAfter: string +} +/** Placeholder values for TLS Client Authorization */ +interface IncomingRequestCfPropertiesTLSClientAuthPlaceholder { + certPresented: '0' + certVerified: 'NONE' + certRevoked: '0' + certIssuerDN: '' + certSubjectDN: '' + certIssuerDNRFC2253: '' + certSubjectDNRFC2253: '' + certIssuerDNLegacy: '' + certSubjectDNLegacy: '' + certSerial: '' + certIssuerSerial: '' + certSKI: '' + certIssuerSKI: '' + certFingerprintSHA1: '' + certFingerprintSHA256: '' + certNotBefore: '' + certNotAfter: '' +} +/** Possible outcomes of TLS verification */ +declare type CertVerificationStatus = + /** Authentication succeeded */ + | 'SUCCESS' + /** No certificate was presented */ + | 'NONE' + /** Failed because the certificate was self-signed */ + | 'FAILED:self signed certificate' + /** Failed because the certificate failed a trust chain check */ + | 'FAILED:unable to verify the first certificate' + /** Failed because the certificate not yet valid */ + | 'FAILED:certificate is not yet valid' + /** Failed because the certificate is expired */ + | 'FAILED:certificate has expired' + /** Failed for another unspecified reason */ + | 'FAILED' +/** + * An upstream endpoint's response to a TCP `keepalive` message from Cloudflare. + */ +declare type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus = + | 0 /** Unknown */ + | 1 /** no keepalives (not found) */ + | 2 /** no connection re-use, opening keepalive connection failed */ + | 3 /** no connection re-use, keepalive accepted and saved */ + | 4 /** connection re-use, refused by the origin server (`TCP FIN`) */ + | 5 /** connection re-use, accepted by the origin server */ +/** ISO 3166-1 Alpha-2 codes */ +declare type Iso3166Alpha2Code = + | 'AD' + | 'AE' + | 'AF' + | 'AG' + | 'AI' + | 'AL' + | 'AM' + | 'AO' + | 'AQ' + | 'AR' + | 'AS' + | 'AT' + | 'AU' + | 'AW' + | 'AX' + | 'AZ' + | 'BA' + | 'BB' + | 'BD' + | 'BE' + | 'BF' + | 'BG' + | 'BH' + | 'BI' + | 'BJ' + | 'BL' + | 'BM' + | 'BN' + | 'BO' + | 'BQ' + | 'BR' + | 'BS' + | 'BT' + | 'BV' + | 'BW' + | 'BY' + | 'BZ' + | 'CA' + | 'CC' + | 'CD' + | 'CF' + | 'CG' + | 'CH' + | 'CI' + | 'CK' + | 'CL' + | 'CM' + | 'CN' + | 'CO' + | 'CR' + | 'CU' + | 'CV' + | 'CW' + | 'CX' + | 'CY' + | 'CZ' + | 'DE' + | 'DJ' + | 'DK' + | 'DM' + | 'DO' + | 'DZ' + | 'EC' + | 'EE' + | 'EG' + | 'EH' + | 'ER' + | 'ES' + | 'ET' + | 'FI' + | 'FJ' + | 'FK' + | 'FM' + | 'FO' + | 'FR' + | 'GA' + | 'GB' + | 'GD' + | 'GE' + | 'GF' + | 'GG' + | 'GH' + | 'GI' + | 'GL' + | 'GM' + | 'GN' + | 'GP' + | 'GQ' + | 'GR' + | 'GS' + | 'GT' + | 'GU' + | 'GW' + | 'GY' + | 'HK' + | 'HM' + | 'HN' + | 'HR' + | 'HT' + | 'HU' + | 'ID' + | 'IE' + | 'IL' + | 'IM' + | 'IN' + | 'IO' + | 'IQ' + | 'IR' + | 'IS' + | 'IT' + | 'JE' + | 'JM' + | 'JO' + | 'JP' + | 'KE' + | 'KG' + | 'KH' + | 'KI' + | 'KM' + | 'KN' + | 'KP' + | 'KR' + | 'KW' + | 'KY' + | 'KZ' + | 'LA' + | 'LB' + | 'LC' + | 'LI' + | 'LK' + | 'LR' + | 'LS' + | 'LT' + | 'LU' + | 'LV' + | 'LY' + | 'MA' + | 'MC' + | 'MD' + | 'ME' + | 'MF' + | 'MG' + | 'MH' + | 'MK' + | 'ML' + | 'MM' + | 'MN' + | 'MO' + | 'MP' + | 'MQ' + | 'MR' + | 'MS' + | 'MT' + | 'MU' + | 'MV' + | 'MW' + | 'MX' + | 'MY' + | 'MZ' + | 'NA' + | 'NC' + | 'NE' + | 'NF' + | 'NG' + | 'NI' + | 'NL' + | 'NO' + | 'NP' + | 'NR' + | 'NU' + | 'NZ' + | 'OM' + | 'PA' + | 'PE' + | 'PF' + | 'PG' + | 'PH' + | 'PK' + | 'PL' + | 'PM' + | 'PN' + | 'PR' + | 'PS' + | 'PT' + | 'PW' + | 'PY' + | 'QA' + | 'RE' + | 'RO' + | 'RS' + | 'RU' + | 'RW' + | 'SA' + | 'SB' + | 'SC' + | 'SD' + | 'SE' + | 'SG' + | 'SH' + | 'SI' + | 'SJ' + | 'SK' + | 'SL' + | 'SM' + | 'SN' + | 'SO' + | 'SR' + | 'SS' + | 'ST' + | 'SV' + | 'SX' + | 'SY' + | 'SZ' + | 'TC' + | 'TD' + | 'TF' + | 'TG' + | 'TH' + | 'TJ' + | 'TK' + | 'TL' + | 'TM' + | 'TN' + | 'TO' + | 'TR' + | 'TT' + | 'TV' + | 'TW' + | 'TZ' + | 'UA' + | 'UG' + | 'UM' + | 'US' + | 'UY' + | 'UZ' + | 'VA' + | 'VC' + | 'VE' + | 'VG' + | 'VI' + | 'VN' + | 'VU' + | 'WF' + | 'WS' + | 'YE' + | 'YT' + | 'ZA' + | 'ZM' + | 'ZW' +/** The 2-letter continent codes Cloudflare uses */ +declare type ContinentCode = 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA' +type CfProperties = + | IncomingRequestCfProperties + | RequestInitCfProperties +interface D1Meta { + duration: number + size_after: number + rows_read: number + rows_written: number + last_row_id: number + changed_db: boolean + changes: number + /** + * The region of the database instance that executed the query. + */ + served_by_region?: string + /** + * True if-and-only-if the database instance that executed the query was the primary. + */ + served_by_primary?: boolean + timings?: { + /** + * The duration of the SQL query execution by the database instance. It doesn't include any network time. + */ + sql_duration_ms: number + } +} +interface D1Response { + success: true + meta: D1Meta & Record + error?: never +} +type D1Result = D1Response & { + results: T[] +} +interface D1ExecResult { + count: number + duration: number +} +type D1SessionConstraint = + // Indicates that the first query should go to the primary, and the rest queries + // using the same D1DatabaseSession will go to any replica that is consistent with + // the bookmark maintained by the session (returned by the first query). + | 'first-primary' + // Indicates that the first query can go anywhere (primary or replica), and the rest queries + // using the same D1DatabaseSession will go to any replica that is consistent with + // the bookmark maintained by the session (returned by the first query). + | 'first-unconstrained' +type D1SessionBookmark = string +declare abstract class D1Database { + prepare(query: string): D1PreparedStatement + batch(statements: D1PreparedStatement[]): Promise[]> + exec(query: string): Promise + /** + * Creates a new D1 Session anchored at the given constraint or the bookmark. + * All queries executed using the created session will have sequential consistency, + * meaning that all writes done through the session will be visible in subsequent reads. + * + * @param constraintOrBookmark Either the session constraint or the explicit bookmark to anchor the created session. + */ + withSession(constraintOrBookmark?: D1SessionBookmark | D1SessionConstraint): D1DatabaseSession + /** + * @deprecated dump() will be removed soon, only applies to deprecated alpha v1 databases. + */ + dump(): Promise +} +declare abstract class D1DatabaseSession { + prepare(query: string): D1PreparedStatement + batch(statements: D1PreparedStatement[]): Promise[]> + /** + * @returns The latest session bookmark across all executed queries on the session. + * If no query has been executed yet, `null` is returned. + */ + getBookmark(): D1SessionBookmark | null +} +declare abstract class D1PreparedStatement { + bind(...values: unknown[]): D1PreparedStatement + first(colName: string): Promise + first>(): Promise + run>(): Promise> + all>(): Promise> + raw(options: { columnNames: true }): Promise<[string[], ...T[]]> + raw(options?: { columnNames?: false }): Promise +} +// `Disposable` was added to TypeScript's standard lib types in version 5.2. +// To support older TypeScript versions, define an empty `Disposable` interface. +// Users won't be able to use `using`/`Symbol.dispose` without upgrading to 5.2, +// but this will ensure type checking on older versions still passes. +// TypeScript's interface merging will ensure our empty interface is effectively +// ignored when `Disposable` is included in the standard lib. +interface Disposable {} +/** + * An email message that can be sent from a Worker. + */ +interface EmailMessage { + /** + * Envelope From attribute of the email message. + */ + readonly from: string + /** + * Envelope To attribute of the email message. + */ + readonly to: string +} +/** + * An email message that is sent to a consumer Worker and can be rejected/forwarded. + */ +interface ForwardableEmailMessage extends EmailMessage { + /** + * Stream of the email message content. + */ + readonly raw: ReadableStream + /** + * An [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers). + */ + readonly headers: Headers + /** + * Size of the email message content. + */ + readonly rawSize: number + /** + * Reject this email message by returning a permanent SMTP error back to the connecting client including the given reason. + * @param reason The reject reason. + * @returns void + */ + setReject(reason: string): void + /** + * Forward this email message to a verified destination address of the account. + * @param rcptTo Verified destination address. + * @param headers A [Headers object](https://developer.mozilla.org/en-US/docs/Web/API/Headers). + * @returns A promise that resolves when the email message is forwarded. + */ + forward(rcptTo: string, headers?: Headers): Promise + /** + * Reply to the sender of this email message with a new EmailMessage object. + * @param message The reply message. + * @returns A promise that resolves when the email message is replied. + */ + reply(message: EmailMessage): Promise +} +/** + * A binding that allows a Worker to send email messages. + */ +interface SendEmail { + send(message: EmailMessage): Promise +} +declare abstract class EmailEvent extends ExtendableEvent { + readonly message: ForwardableEmailMessage +} +declare type EmailExportedHandler = ( + message: ForwardableEmailMessage, + env: Env, + ctx: ExecutionContext +) => void | Promise +declare module 'cloudflare:email' { + let _EmailMessage: { + prototype: EmailMessage + new (from: string, to: string, raw: ReadableStream | string): EmailMessage + } + export { _EmailMessage as EmailMessage } +} +interface Hyperdrive { + /** + * Connect directly to Hyperdrive as if it's your database, returning a TCP socket. + * + * Calling this method returns an idential socket to if you call + * `connect("host:port")` using the `host` and `port` fields from this object. + * Pick whichever approach works better with your preferred DB client library. + * + * Note that this socket is not yet authenticated -- it's expected that your + * code (or preferably, the client library of your choice) will authenticate + * using the information in this class's readonly fields. + */ + connect(): Socket + /** + * A valid DB connection string that can be passed straight into the typical + * client library/driver/ORM. This will typically be the easiest way to use + * Hyperdrive. + */ + readonly connectionString: string + /* + * A randomly generated hostname that is only valid within the context of the + * currently running Worker which, when passed into `connect()` function from + * the "cloudflare:sockets" module, will connect to the Hyperdrive instance + * for your database. + */ + readonly host: string + /* + * The port that must be paired the the host field when connecting. + */ + readonly port: number + /* + * The username to use when authenticating to your database via Hyperdrive. + * Unlike the host and password, this will be the same every time + */ + readonly user: string + /* + * The randomly generated password to use when authenticating to your + * database via Hyperdrive. Like the host field, this password is only valid + * within the context of the currently running Worker instance from which + * it's read. + */ + readonly password: string + /* + * The name of the database to connect to. + */ + readonly database: string +} +// Copyright (c) 2024 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +type ImageInfoResponse = + | { + format: 'image/svg+xml' + } + | { + format: string + fileSize: number + width: number + height: number + } +type ImageTransform = { + width?: number + height?: number + background?: string + blur?: number + border?: + | { + color?: string + width?: number + } + | { + top?: number + bottom?: number + left?: number + right?: number + } + brightness?: number + contrast?: number + fit?: 'scale-down' | 'contain' | 'pad' | 'squeeze' | 'cover' | 'crop' + flip?: 'h' | 'v' | 'hv' + gamma?: number + gravity?: + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'center' + | 'auto' + | 'entropy' + | { + x?: number + y?: number + mode: 'remainder' | 'box-center' + } + rotate?: 0 | 90 | 180 | 270 + saturation?: number + sharpen?: number + trim?: + | 'border' + | { + top?: number + bottom?: number + left?: number + right?: number + width?: number + height?: number + border?: + | boolean + | { + color?: string + tolerance?: number + keep?: number + } + } +} +type ImageDrawOptions = { + opacity?: number + repeat?: boolean | string + top?: number + left?: number + bottom?: number + right?: number +} +type ImageOutputOptions = { + format: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp' | 'image/avif' | 'rgb' | 'rgba' + quality?: number + background?: string +} +interface ImagesBinding { + /** + * Get image metadata (type, width and height) + * @throws {@link ImagesError} with code 9412 if input is not an image + * @param stream The image bytes + */ + info(stream: ReadableStream): Promise + /** + * Begin applying a series of transformations to an image + * @param stream The image bytes + * @returns A transform handle + */ + input(stream: ReadableStream): ImageTransformer +} +interface ImageTransformer { + /** + * Apply transform next, returning a transform handle. + * You can then apply more transformations, draw, or retrieve the output. + * @param transform + */ + transform(transform: ImageTransform): ImageTransformer + /** + * Draw an image on this transformer, returning a transform handle. + * You can then apply more transformations, draw, or retrieve the output. + * @param image The image (or transformer that will give the image) to draw + * @param options The options configuring how to draw the image + */ + draw( + image: ReadableStream | ImageTransformer, + options?: ImageDrawOptions + ): ImageTransformer + /** + * Retrieve the image that results from applying the transforms to the + * provided input + * @param options Options that apply to the output e.g. output format + */ + output(options: ImageOutputOptions): Promise +} +interface ImageTransformationResult { + /** + * The image as a response, ready to store in cache or return to users + */ + response(): Response + /** + * The content type of the returned image + */ + contentType(): string + /** + * The bytes of the response + */ + image(): ReadableStream +} +interface ImagesError extends Error { + readonly code: number + readonly message: string + readonly stack?: string +} +type Params

= Record +type EventContext = { + request: Request> + functionPath: string + waitUntil: (promise: Promise) => void + passThroughOnException: () => void + next: (input?: Request | string, init?: RequestInit) => Promise + env: Env & { + ASSETS: { + fetch: typeof fetch + } + } + params: Params

+ data: Data +} +type PagesFunction< + Env = unknown, + Params extends string = any, + Data extends Record = Record, +> = (context: EventContext) => Response | Promise +type EventPluginContext = { + request: Request> + functionPath: string + waitUntil: (promise: Promise) => void + passThroughOnException: () => void + next: (input?: Request | string, init?: RequestInit) => Promise + env: Env & { + ASSETS: { + fetch: typeof fetch + } + } + params: Params

+ data: Data + pluginArgs: PluginArgs +} +type PagesPluginFunction< + Env = unknown, + Params extends string = any, + Data extends Record = Record, + PluginArgs = unknown, +> = (context: EventPluginContext) => Response | Promise +declare module 'assets:*' { + export const onRequest: PagesFunction +} +// Copyright (c) 2022-2023 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +declare module 'cloudflare:pipelines' { + export abstract class PipelineTransformationEntrypoint< + Env = unknown, + I extends PipelineRecord = PipelineRecord, + O extends PipelineRecord = PipelineRecord, + > { + protected env: Env + protected ctx: ExecutionContext + constructor(ctx: ExecutionContext, env: Env) + /** + * run recieves an array of PipelineRecord which can be + * transformed and returned to the pipeline + * @param records Incoming records from the pipeline to be transformed + * @param metadata Information about the specific pipeline calling the transformation entrypoint + * @returns A promise containing the transformed PipelineRecord array + */ + public run(records: I[], metadata: PipelineBatchMetadata): Promise + } + export type PipelineRecord = Record + export type PipelineBatchMetadata = { + pipelineId: string + pipelineName: string + } + export interface Pipeline { + /** + * The Pipeline interface represents the type of a binding to a Pipeline + * + * @param records The records to send to the pipeline + */ + send(records: T[]): Promise + } +} +// PubSubMessage represents an incoming PubSub message. +// The message includes metadata about the broker, the client, and the payload +// itself. +// https://developers.cloudflare.com/pub-sub/ +interface PubSubMessage { + // Message ID + readonly mid: number + // MQTT broker FQDN in the form mqtts://BROKER.NAMESPACE.cloudflarepubsub.com:PORT + readonly broker: string + // The MQTT topic the message was sent on. + readonly topic: string + // The client ID of the client that published this message. + readonly clientId: string + // The unique identifier (JWT ID) used by the client to authenticate, if token + // auth was used. + readonly jti?: string + // A Unix timestamp (seconds from Jan 1, 1970), set when the Pub/Sub Broker + // received the message from the client. + readonly receivedAt: number + // An (optional) string with the MIME type of the payload, if set by the + // client. + readonly contentType: string + // Set to 1 when the payload is a UTF-8 string + // https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901063 + readonly payloadFormatIndicator: number + // Pub/Sub (MQTT) payloads can be UTF-8 strings, or byte arrays. + // You can use payloadFormatIndicator to inspect this before decoding. + payload: string | Uint8Array +} +// JsonWebKey extended by kid parameter +interface JsonWebKeyWithKid extends JsonWebKey { + // Key Identifier of the JWK + readonly kid: string +} +interface RateLimitOptions { + key: string +} +interface RateLimitOutcome { + success: boolean +} +interface RateLimit { + /** + * Rate limit a request based on the provided options. + * @see https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/ + * @returns A promise that resolves with the outcome of the rate limit. + */ + limit(options: RateLimitOptions): Promise +} +// Namespace for RPC utility types. Unfortunately, we can't use a `module` here as these types need +// to referenced by `Fetcher`. This is included in the "importable" version of the types which +// strips all `module` blocks. +declare namespace Rpc { + // Branded types for identifying `WorkerEntrypoint`/`DurableObject`/`Target`s. + // TypeScript uses *structural* typing meaning anything with the same shape as type `T` is a `T`. + // For the classes exported by `cloudflare:workers` we want *nominal* typing (i.e. we only want to + // accept `WorkerEntrypoint` from `cloudflare:workers`, not any other class with the same shape) + export const __RPC_STUB_BRAND: '__RPC_STUB_BRAND' + export const __RPC_TARGET_BRAND: '__RPC_TARGET_BRAND' + export const __WORKER_ENTRYPOINT_BRAND: '__WORKER_ENTRYPOINT_BRAND' + export const __DURABLE_OBJECT_BRAND: '__DURABLE_OBJECT_BRAND' + export const __WORKFLOW_ENTRYPOINT_BRAND: '__WORKFLOW_ENTRYPOINT_BRAND' + export interface RpcTargetBranded { + [__RPC_TARGET_BRAND]: never + } + export interface WorkerEntrypointBranded { + [__WORKER_ENTRYPOINT_BRAND]: never + } + export interface DurableObjectBranded { + [__DURABLE_OBJECT_BRAND]: never + } + export interface WorkflowEntrypointBranded { + [__WORKFLOW_ENTRYPOINT_BRAND]: never + } + export type EntrypointBranded = + | WorkerEntrypointBranded + | DurableObjectBranded + | WorkflowEntrypointBranded + // Types that can be used through `Stub`s + export type Stubable = RpcTargetBranded | ((...args: any[]) => any) + // Types that can be passed over RPC + // The reason for using a generic type here is to build a serializable subset of structured + // cloneable composite types. This allows types defined with the "interface" keyword to pass the + // serializable check as well. Otherwise, only types defined with the "type" keyword would pass. + type Serializable = + // Structured cloneables + | BaseType + // Structured cloneable composites + | Map< + T extends Map ? Serializable : never, + T extends Map ? Serializable : never + > + | Set ? Serializable : never> + | ReadonlyArray ? Serializable : never> + | { + [K in keyof T]: K extends number | string ? Serializable : never + } + // Special types + | Stub + // Serialized as stubs, see `Stubify` + | Stubable + // Base type for all RPC stubs, including common memory management methods. + // `T` is used as a marker type for unwrapping `Stub`s later. + interface StubBase extends Disposable { + [__RPC_STUB_BRAND]: T + dup(): this + } + export type Stub = Provider & StubBase + // This represents all the types that can be sent as-is over an RPC boundary + type BaseType = + | void + | undefined + | null + | boolean + | number + | bigint + | string + | TypedArray + | ArrayBuffer + | DataView + | Date + | Error + | RegExp + | ReadableStream + | WritableStream + | Request + | Response + | Headers + // Recursively rewrite all `Stubable` types with `Stub`s + // prettier-ignore + type Stubify = T extends Stubable ? Stub : T extends Map ? Map, Stubify> : T extends Set ? Set> : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends BaseType ? T : T extends { + [key: string | number]: any; + } ? { + [K in keyof T]: Stubify; + } : T; + // Recursively rewrite all `Stub`s with the corresponding `T`s. + // Note we use `StubBase` instead of `Stub` here to avoid circular dependencies: + // `Stub` depends on `Provider`, which depends on `Unstubify`, which would depend on `Stub`. + // prettier-ignore + type Unstubify = T extends StubBase ? V : T extends Map ? Map, Unstubify> : T extends Set ? Set> : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends BaseType ? T : T extends { + [key: string | number]: unknown; + } ? { + [K in keyof T]: Unstubify; + } : T; + type UnstubifyAll = { + [I in keyof A]: Unstubify + } + // Utility type for adding `Provider`/`Disposable`s to `object` types only. + // Note `unknown & T` is equivalent to `T`. + type MaybeProvider = T extends object ? Provider : unknown + type MaybeDisposable = T extends object ? Disposable : unknown + // Type for method return or property on an RPC interface. + // - Stubable types are replaced by stubs. + // - Serializable types are passed by value, with stubable types replaced by stubs + // and a top-level `Disposer`. + // Everything else can't be passed over PRC. + // Technically, we use custom thenables here, but they quack like `Promise`s. + // Intersecting with `(Maybe)Provider` allows pipelining. + // prettier-ignore + type Result = R extends Stubable ? Promise> & Provider : R extends Serializable ? Promise & MaybeDisposable> & MaybeProvider : never; + // Type for method or property on an RPC interface. + // For methods, unwrap `Stub`s in parameters, and rewrite returns to be `Result`s. + // Unwrapping `Stub`s allows calling with `Stubable` arguments. + // For properties, rewrite types to be `Result`s. + // In each case, unwrap `Promise`s. + type MethodOrProperty = V extends (...args: infer P) => infer R + ? (...args: UnstubifyAll

) => Result> + : Result> + // Type for the callable part of an `Provider` if `T` is callable. + // This is intersected with methods/properties. + type MaybeCallableProvider = T extends (...args: any[]) => any ? MethodOrProperty : unknown + // Base type for all other types providing RPC-like interfaces. + // Rewrites all methods/properties to be `MethodOrProperty`s, while preserving callable types. + // `Reserved` names (e.g. stub method names like `dup()`) and symbols can't be accessed over RPC. + export type Provider< + T extends object, + Reserved extends string = never, + > = MaybeCallableProvider & { + [K in Exclude>]: MethodOrProperty + } +} +declare namespace Cloudflare { + interface Env {} +} +declare module 'cloudflare:workers' { + export type RpcStub = Rpc.Stub + export const RpcStub: { + new (value: T): Rpc.Stub + } + export abstract class RpcTarget implements Rpc.RpcTargetBranded { + [Rpc.__RPC_TARGET_BRAND]: never + } + // `protected` fields don't appear in `keyof`s, so can't be accessed over RPC + export abstract class WorkerEntrypoint implements Rpc.WorkerEntrypointBranded { + [Rpc.__WORKER_ENTRYPOINT_BRAND]: never + protected ctx: ExecutionContext + protected env: Env + constructor(ctx: ExecutionContext, env: Env) + fetch?(request: Request): Response | Promise + tail?(events: TraceItem[]): void | Promise + trace?(traces: TraceItem[]): void | Promise + scheduled?(controller: ScheduledController): void | Promise + queue?(batch: MessageBatch): void | Promise + test?(controller: TestController): void | Promise + } + export abstract class DurableObject implements Rpc.DurableObjectBranded { + [Rpc.__DURABLE_OBJECT_BRAND]: never + protected ctx: DurableObjectState + protected env: Env + constructor(ctx: DurableObjectState, env: Env) + fetch?(request: Request): Response | Promise + alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise + webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise + webSocketClose?( + ws: WebSocket, + code: number, + reason: string, + wasClean: boolean + ): void | Promise + webSocketError?(ws: WebSocket, error: unknown): void | Promise + } + export type WorkflowDurationLabel = + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'month' + | 'year' + export type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number + export type WorkflowDelayDuration = WorkflowSleepDuration + export type WorkflowTimeoutDuration = WorkflowSleepDuration + export type WorkflowRetentionDuration = WorkflowSleepDuration + export type WorkflowBackoff = 'constant' | 'linear' | 'exponential' + export type WorkflowStepConfig = { + retries?: { + limit: number + delay: WorkflowDelayDuration | number + backoff?: WorkflowBackoff + } + timeout?: WorkflowTimeoutDuration | number + } + export type WorkflowEvent = { + payload: Readonly + timestamp: Date + instanceId: string + } + export type WorkflowStepEvent = { + payload: Readonly + timestamp: Date + type: string + } + export abstract class WorkflowStep { + do>(name: string, callback: () => Promise): Promise + do>( + name: string, + config: WorkflowStepConfig, + callback: () => Promise + ): Promise + sleep: (name: string, duration: WorkflowSleepDuration) => Promise + sleepUntil: (name: string, timestamp: Date | number) => Promise + waitForEvent>( + name: string, + options: { + type: string + timeout?: WorkflowTimeoutDuration | number + } + ): Promise> + } + export abstract class WorkflowEntrypoint< + Env = unknown, + T extends Rpc.Serializable | unknown = unknown, + > + implements Rpc.WorkflowEntrypointBranded + { + [Rpc.__WORKFLOW_ENTRYPOINT_BRAND]: never + protected ctx: ExecutionContext + protected env: Env + constructor(ctx: ExecutionContext, env: Env) + run(event: Readonly>, step: WorkflowStep): Promise + } + export const env: Cloudflare.Env +} +interface SecretsStoreSecret { + /** + * Get a secret from the Secrets Store, returning a string of the secret value + * if it exists, or throws an error if it does not exist + */ + get(): Promise +} +declare module 'cloudflare:sockets' { + function _connect(address: string | SocketAddress, options?: SocketOptions): Socket + export { _connect as connect } +} +declare namespace TailStream { + interface Header { + readonly name: string + readonly value: string + } + interface FetchEventInfo { + readonly type: 'fetch' + readonly method: string + readonly url: string + readonly cfJson: string + readonly headers: Header[] + } + interface JsRpcEventInfo { + readonly type: 'jsrpc' + readonly methodName: string + } + interface ScheduledEventInfo { + readonly type: 'scheduled' + readonly scheduledTime: Date + readonly cron: string + } + interface AlarmEventInfo { + readonly type: 'alarm' + readonly scheduledTime: Date + } + interface QueueEventInfo { + readonly type: 'queue' + readonly queueName: string + readonly batchSize: number + } + interface EmailEventInfo { + readonly type: 'email' + readonly mailFrom: string + readonly rcptTo: string + readonly rawSize: number + } + interface TraceEventInfo { + readonly type: 'trace' + readonly traces: (string | null)[] + } + interface HibernatableWebSocketEventInfoMessage { + readonly type: 'message' + } + interface HibernatableWebSocketEventInfoError { + readonly type: 'error' + } + interface HibernatableWebSocketEventInfoClose { + readonly type: 'close' + readonly code: number + readonly wasClean: boolean + } + interface HibernatableWebSocketEventInfo { + readonly type: 'hibernatableWebSocket' + readonly info: + | HibernatableWebSocketEventInfoClose + | HibernatableWebSocketEventInfoError + | HibernatableWebSocketEventInfoMessage + } + interface Resume { + readonly type: 'resume' + readonly attachment?: any + } + interface CustomEventInfo { + readonly type: 'custom' + } + interface FetchResponseInfo { + readonly type: 'fetch' + readonly statusCode: number + } + type EventOutcome = + | 'ok' + | 'canceled' + | 'exception' + | 'unknown' + | 'killSwitch' + | 'daemonDown' + | 'exceededCpu' + | 'exceededMemory' + | 'loadShed' + | 'responseStreamDisconnected' + | 'scriptNotFound' + interface ScriptVersion { + readonly id: string + readonly tag?: string + readonly message?: string + } + interface Trigger { + readonly traceId: string + readonly invocationId: string + readonly spanId: string + } + interface Onset { + readonly type: 'onset' + readonly dispatchNamespace?: string + readonly entrypoint?: string + readonly executionModel: string + readonly scriptName?: string + readonly scriptTags?: string[] + readonly scriptVersion?: ScriptVersion + readonly trigger?: Trigger + readonly info: + | FetchEventInfo + | JsRpcEventInfo + | ScheduledEventInfo + | AlarmEventInfo + | QueueEventInfo + | EmailEventInfo + | TraceEventInfo + | HibernatableWebSocketEventInfo + | Resume + | CustomEventInfo + } + interface Outcome { + readonly type: 'outcome' + readonly outcome: EventOutcome + readonly cpuTime: number + readonly wallTime: number + } + interface Hibernate { + readonly type: 'hibernate' + } + interface SpanOpen { + readonly type: 'spanOpen' + readonly name: string + readonly info?: FetchEventInfo | JsRpcEventInfo | Attributes + } + interface SpanClose { + readonly type: 'spanClose' + readonly outcome: EventOutcome + } + interface DiagnosticChannelEvent { + readonly type: 'diagnosticChannel' + readonly channel: string + readonly message: any + } + interface Exception { + readonly type: 'exception' + readonly name: string + readonly message: string + readonly stack?: string + } + interface Log { + readonly type: 'log' + readonly level: 'debug' | 'error' | 'info' | 'log' | 'warn' + readonly message: string + } + interface Return { + readonly type: 'return' + readonly info?: FetchResponseInfo + } + interface Link { + readonly type: 'link' + readonly label?: string + readonly traceId: string + readonly invocationId: string + readonly spanId: string + } + interface Attribute { + readonly name: string + readonly value: string | string[] | boolean | boolean[] | number | number[] | bigint | bigint[] + } + interface Attributes { + readonly type: 'attributes' + readonly info: Attribute[] + } + interface TailEvent { + readonly traceId: string + readonly invocationId: string + readonly spanId: string + readonly timestamp: Date + readonly sequence: number + readonly event: + | Onset + | Outcome + | Hibernate + | SpanOpen + | SpanClose + | DiagnosticChannelEvent + | Exception + | Log + | Return + | Link + | Attributes + } + type TailEventHandler = (event: TailEvent) => void | Promise + type TailEventHandlerName = + | 'outcome' + | 'hibernate' + | 'spanOpen' + | 'spanClose' + | 'diagnosticChannel' + | 'exception' + | 'log' + | 'return' + | 'link' + | 'attributes' + type TailEventHandlerObject = Record + type TailEventHandlerType = TailEventHandler | TailEventHandlerObject +} +// Copyright (c) 2022-2023 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +/** + * Data types supported for holding vector metadata. + */ +type VectorizeVectorMetadataValue = string | number | boolean | string[] +/** + * Additional information to associate with a vector. + */ +type VectorizeVectorMetadata = + | VectorizeVectorMetadataValue + | Record +type VectorFloatArray = Float32Array | Float64Array +interface VectorizeError { + code?: number + error: string +} +/** + * Comparison logic/operation to use for metadata filtering. + * + * This list is expected to grow as support for more operations are released. + */ +type VectorizeVectorMetadataFilterOp = '$eq' | '$ne' +/** + * Filter criteria for vector metadata used to limit the retrieved query result set. + */ +type VectorizeVectorMetadataFilter = { + [field: string]: + | Exclude + | null + | { + [Op in VectorizeVectorMetadataFilterOp]?: Exclude< + VectorizeVectorMetadataValue, + string[] + > | null + } +} +/** + * Supported distance metrics for an index. + * Distance metrics determine how other "similar" vectors are determined. + */ +type VectorizeDistanceMetric = 'euclidean' | 'cosine' | 'dot-product' +/** + * Metadata return levels for a Vectorize query. + * + * Default to "none". + * + * @property all Full metadata for the vector return set, including all fields (including those un-indexed) without truncation. This is a more expensive retrieval, as it requires additional fetching & reading of un-indexed data. + * @property indexed Return all metadata fields configured for indexing in the vector return set. This level of retrieval is "free" in that no additional overhead is incurred returning this data. However, note that indexed metadata is subject to truncation (especially for larger strings). + * @property none No indexed metadata will be returned. + */ +type VectorizeMetadataRetrievalLevel = 'all' | 'indexed' | 'none' +interface VectorizeQueryOptions { + topK?: number + namespace?: string + returnValues?: boolean + returnMetadata?: boolean | VectorizeMetadataRetrievalLevel + filter?: VectorizeVectorMetadataFilter +} +/** + * Information about the configuration of an index. + */ +type VectorizeIndexConfig = + | { + dimensions: number + metric: VectorizeDistanceMetric + } + | { + preset: string // keep this generic, as we'll be adding more presets in the future and this is only in a read capacity + } +/** + * Metadata about an existing index. + * + * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released. + * See {@link VectorizeIndexInfo} for its post-beta equivalent. + */ +interface VectorizeIndexDetails { + /** The unique ID of the index */ + readonly id: string + /** The name of the index. */ + name: string + /** (optional) A human readable description for the index. */ + description?: string + /** The index configuration, including the dimension size and distance metric. */ + config: VectorizeIndexConfig + /** The number of records containing vectors within the index. */ + vectorsCount: number +} +/** + * Metadata about an existing index. + */ +interface VectorizeIndexInfo { + /** The number of records containing vectors within the index. */ + vectorCount: number + /** Number of dimensions the index has been configured for. */ + dimensions: number + /** ISO 8601 datetime of the last processed mutation on in the index. All changes before this mutation will be reflected in the index state. */ + processedUpToDatetime: number + /** UUIDv4 of the last mutation processed by the index. All changes before this mutation will be reflected in the index state. */ + processedUpToMutation: number +} +/** + * Represents a single vector value set along with its associated metadata. + */ +interface VectorizeVector { + /** The ID for the vector. This can be user-defined, and must be unique. It should uniquely identify the object, and is best set based on the ID of what the vector represents. */ + id: string + /** The vector values */ + values: VectorFloatArray | number[] + /** The namespace this vector belongs to. */ + namespace?: string + /** Metadata associated with the vector. Includes the values of other fields and potentially additional details. */ + metadata?: Record +} +/** + * Represents a matched vector for a query along with its score and (if specified) the matching vector information. + */ +type VectorizeMatch = Pick, 'values'> & + Omit & { + /** The score or rank for similarity, when returned as a result */ + score: number + } +/** + * A set of matching {@link VectorizeMatch} for a particular query. + */ +interface VectorizeMatches { + matches: VectorizeMatch[] + count: number +} +/** + * Results of an operation that performed a mutation on a set of vectors. + * Here, `ids` is a list of vectors that were successfully processed. + * + * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released. + * See {@link VectorizeAsyncMutation} for its post-beta equivalent. + */ +interface VectorizeVectorMutation { + /* List of ids of vectors that were successfully processed. */ + ids: string[] + /* Total count of the number of processed vectors. */ + count: number +} +/** + * Result type indicating a mutation on the Vectorize Index. + * Actual mutations are processed async where the `mutationId` is the unique identifier for the operation. + */ +interface VectorizeAsyncMutation { + /** The unique identifier for the async mutation operation containing the changeset. */ + mutationId: string +} +/** + * A Vectorize Vector Search Index for querying vectors/embeddings. + * + * This type is exclusively for the Vectorize **beta** and will be deprecated once Vectorize RC is released. + * See {@link Vectorize} for its new implementation. + */ +declare abstract class VectorizeIndex { + /** + * Get information about the currently bound index. + * @returns A promise that resolves with information about the current index. + */ + public describe(): Promise + /** + * Use the provided vector to perform a similarity search across the index. + * @param vector Input vector that will be used to drive the similarity search. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public query( + vector: VectorFloatArray | number[], + options?: VectorizeQueryOptions + ): Promise + /** + * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown. + * @param vectors List of vectors that will be inserted. + * @returns A promise that resolves with the ids & count of records that were successfully processed. + */ + public insert(vectors: VectorizeVector[]): Promise + /** + * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values. + * @param vectors List of vectors that will be upserted. + * @returns A promise that resolves with the ids & count of records that were successfully processed. + */ + public upsert(vectors: VectorizeVector[]): Promise + /** + * Delete a list of vectors with a matching id. + * @param ids List of vector ids that should be deleted. + * @returns A promise that resolves with the ids & count of records that were successfully processed (and thus deleted). + */ + public deleteByIds(ids: string[]): Promise + /** + * Get a list of vectors with a matching id. + * @param ids List of vector ids that should be returned. + * @returns A promise that resolves with the raw unscored vectors matching the id set. + */ + public getByIds(ids: string[]): Promise +} +/** + * A Vectorize Vector Search Index for querying vectors/embeddings. + * + * Mutations in this version are async, returning a mutation id. + */ +declare abstract class Vectorize { + /** + * Get information about the currently bound index. + * @returns A promise that resolves with information about the current index. + */ + public describe(): Promise + /** + * Use the provided vector to perform a similarity search across the index. + * @param vector Input vector that will be used to drive the similarity search. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public query( + vector: VectorFloatArray | number[], + options?: VectorizeQueryOptions + ): Promise + /** + * Use the provided vector-id to perform a similarity search across the index. + * @param vectorId Id for a vector in the index against which the index should be queried. + * @param options Configuration options to massage the returned data. + * @returns A promise that resolves with matched and scored vectors. + */ + public queryById(vectorId: string, options?: VectorizeQueryOptions): Promise + /** + * Insert a list of vectors into the index dataset. If a provided id exists, an error will be thrown. + * @param vectors List of vectors that will be inserted. + * @returns A promise that resolves with a unique identifier of a mutation containing the insert changeset. + */ + public insert(vectors: VectorizeVector[]): Promise + /** + * Upsert a list of vectors into the index dataset. If a provided id exists, it will be replaced with the new values. + * @param vectors List of vectors that will be upserted. + * @returns A promise that resolves with a unique identifier of a mutation containing the upsert changeset. + */ + public upsert(vectors: VectorizeVector[]): Promise + /** + * Delete a list of vectors with a matching id. + * @param ids List of vector ids that should be deleted. + * @returns A promise that resolves with a unique identifier of a mutation containing the delete changeset. + */ + public deleteByIds(ids: string[]): Promise + /** + * Get a list of vectors with a matching id. + * @param ids List of vector ids that should be returned. + * @returns A promise that resolves with the raw unscored vectors matching the id set. + */ + public getByIds(ids: string[]): Promise +} +/** + * The interface for "version_metadata" binding + * providing metadata about the Worker Version using this binding. + */ +type WorkerVersionMetadata = { + /** The ID of the Worker Version using this binding */ + id: string + /** The tag of the Worker Version using this binding */ + tag: string + /** The timestamp of when the Worker Version was uploaded */ + timestamp: string +} +interface DynamicDispatchLimits { + /** + * Limit CPU time in milliseconds. + */ + cpuMs?: number + /** + * Limit number of subrequests. + */ + subRequests?: number +} +interface DynamicDispatchOptions { + /** + * Limit resources of invoked Worker script. + */ + limits?: DynamicDispatchLimits + /** + * Arguments for outbound Worker script, if configured. + */ + outbound?: { + [key: string]: any + } +} +interface DispatchNamespace { + /** + * @param name Name of the Worker script. + * @param args Arguments to Worker script. + * @param options Options for Dynamic Dispatch invocation. + * @returns A Fetcher object that allows you to send requests to the Worker script. + * @throws If the Worker script does not exist in this dispatch namespace, an error will be thrown. + */ + get( + name: string, + args?: { + [key: string]: any + }, + options?: DynamicDispatchOptions + ): Fetcher +} +declare module 'cloudflare:workflows' { + /** + * NonRetryableError allows for a user to throw a fatal error + * that makes a Workflow instance fail immediately without triggering a retry + */ + export class NonRetryableError extends Error { + public constructor(message: string, name?: string) + } +} +declare abstract class Workflow { + /** + * Get a handle to an existing instance of the Workflow. + * @param id Id for the instance of this Workflow + * @returns A promise that resolves with a handle for the Instance + */ + public get(id: string): Promise + /** + * Create a new instance and return a handle to it. If a provided id exists, an error will be thrown. + * @param options Options when creating an instance including id and params + * @returns A promise that resolves with a handle for the Instance + */ + public create(options?: WorkflowInstanceCreateOptions): Promise + /** + * Create a batch of instances and return handle for all of them. If a provided id exists, an error will be thrown. + * `createBatch` is limited at 100 instances at a time or when the RPC limit for the batch (1MiB) is reached. + * @param batch List of Options when creating an instance including name and params + * @returns A promise that resolves with a list of handles for the created instances. + */ + public createBatch(batch: WorkflowInstanceCreateOptions[]): Promise +} +type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' +type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ''}` | number +type WorkflowRetentionDuration = WorkflowSleepDuration +interface WorkflowInstanceCreateOptions { + /** + * An id for your Workflow instance. Must be unique within the Workflow. + */ + id?: string + /** + * The event payload the Workflow instance is triggered with + */ + params?: PARAMS + /** + * The retention policy for Workflow instance. + * Defaults to the maximum retention period available for the owner's account. + */ + retention?: { + successRetention?: WorkflowRetentionDuration + errorRetention?: WorkflowRetentionDuration + } +} +type InstanceStatus = { + status: + | 'queued' // means that instance is waiting to be started (see concurrency limits) + | 'running' + | 'paused' + | 'errored' + | 'terminated' // user terminated the instance while it was running + | 'complete' + | 'waiting' // instance is hibernating and waiting for sleep or event to finish + | 'waitingForPause' // instance is finishing the current work to pause + | 'unknown' + error?: string + output?: object +} +interface WorkflowError { + code?: number + message: string +} +declare abstract class WorkflowInstance { + public id: string + /** + * Pause the instance. + */ + public pause(): Promise + /** + * Resume the instance. If it is already running, an error will be thrown. + */ + public resume(): Promise + /** + * Terminate the instance. If it is errored, terminated or complete, an error will be thrown. + */ + public terminate(): Promise + /** + * Restart the instance. + */ + public restart(): Promise + /** + * Returns the current status of the instance. + */ + public status(): Promise + /** + * Send an event to this instance. + */ + public sendEvent({ type, payload }: { type: string; payload: unknown }): Promise +}