stitchkit 0.116.0 → 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/agent-runtime/run-acquisition.d.ts +9 -2
  3. package/dist/agent-runtime/run-acquisition.d.ts.map +1 -1
  4. package/dist/agent-runtime/run-execution.d.ts +3 -0
  5. package/dist/agent-runtime/run-execution.d.ts.map +1 -1
  6. package/dist/agent-runtime/run-failure.d.ts +4 -1
  7. package/dist/agent-runtime/run-failure.d.ts.map +1 -1
  8. package/dist/agent-runtime/run-store-retry.d.ts +8 -0
  9. package/dist/agent-runtime/run-store-retry.d.ts.map +1 -0
  10. package/dist/agent-runtime/run-terminal.d.ts.map +1 -1
  11. package/dist/agent-runtime/run-unacquired.d.ts +19 -0
  12. package/dist/agent-runtime/run-unacquired.d.ts.map +1 -0
  13. package/dist/agent-runtime/store-conformance.d.ts +8 -0
  14. package/dist/agent-runtime/store-conformance.d.ts.map +1 -1
  15. package/dist/agent-runtime/store-driver-contract.d.ts +5 -0
  16. package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
  17. package/dist/agent-runtime/terminal-commit.d.ts +34 -4
  18. package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
  19. package/dist/entrypoints/agent-runtime/harness.js +2 -2
  20. package/dist/entrypoints/agent-runtime/sqlite/bun.js +3 -3
  21. package/dist/entrypoints/agent-runtime/sqlite/node.js +3 -3
  22. package/dist/entrypoints/agent-runtime/testing.js +22 -4
  23. package/dist/entrypoints/agent-runtime.d.ts +1 -1
  24. package/dist/entrypoints/agent-runtime.d.ts.map +1 -1
  25. package/dist/entrypoints/agent-runtime.js +13 -11
  26. package/dist/entrypoints/application/diagnostic-journal.js +1 -1
  27. package/dist/entrypoints/application/directory-inbox.js +3 -3
  28. package/dist/entrypoints/cli/publish.js +2 -2
  29. package/dist/entrypoints/files.js +2 -2
  30. package/dist/entrypoints/primitives.js +51 -154
  31. package/dist/entrypoints/server.js +6 -6
  32. package/dist/entrypoints/telegram.d.ts +1 -1
  33. package/dist/entrypoints/telegram.d.ts.map +1 -1
  34. package/dist/entrypoints/telegram.js +352 -137
  35. package/dist/entrypoints/tracking/contract.d.ts +11 -0
  36. package/dist/entrypoints/tracking/contract.d.ts.map +1 -0
  37. package/dist/entrypoints/tracking/contract.js +162 -0
  38. package/dist/entrypoints/tracking/performance.d.ts +8 -0
  39. package/dist/entrypoints/tracking/performance.d.ts.map +1 -0
  40. package/dist/entrypoints/tracking/performance.js +181 -0
  41. package/dist/entrypoints/tracking/server.d.ts +1 -0
  42. package/dist/entrypoints/tracking/server.d.ts.map +1 -1
  43. package/dist/entrypoints/tracking/server.js +40 -12
  44. package/dist/entrypoints/tracking.d.ts +4 -3
  45. package/dist/entrypoints/tracking.d.ts.map +1 -1
  46. package/dist/entrypoints/tracking.js +43 -143
  47. package/dist/entrypoints/xlsx.d.ts +4 -0
  48. package/dist/entrypoints/xlsx.d.ts.map +1 -0
  49. package/dist/entrypoints/xlsx.js +406 -0
  50. package/dist/{index-fa435yq2.js → index-073nw9qr.js} +8 -8
  51. package/dist/index-1f40z584.js +7 -0
  52. package/dist/{index-fdk4r4r6.js → index-75ns2ybj.js} +1 -1
  53. package/dist/{index-jq2ky8fw.js → index-cycp3r3e.js} +124 -181
  54. package/dist/{index-4g6qmgdx.js → index-ghtpfz4z.js} +9 -9
  55. package/dist/{index-n3v695sb.js → index-hdjmdwsj.js} +277 -96
  56. package/dist/{index-zqkz4af9.js → index-nqxrm176.js} +1 -1
  57. package/dist/index-qezhmp51.js +242 -0
  58. package/dist/index-vv1sch8a.js +94 -0
  59. package/dist/{index-fggxgbrk.js → index-xx1j310k.js} +165 -102
  60. package/dist/internal/exclusive-lock.d.ts.map +1 -1
  61. package/dist/primitives/decimal.d.ts +10 -0
  62. package/dist/primitives/decimal.d.ts.map +1 -1
  63. package/dist/primitives/index.d.ts +3 -1
  64. package/dist/primitives/index.d.ts.map +1 -1
  65. package/dist/primitives/money.d.ts +20 -0
  66. package/dist/primitives/money.d.ts.map +1 -1
  67. package/dist/primitives/quantity.d.ts.map +1 -1
  68. package/dist/primitives/rounding.d.ts +40 -0
  69. package/dist/primitives/rounding.d.ts.map +1 -0
  70. package/dist/telegram/budget-transformer.d.ts +2 -2
  71. package/dist/telegram/not-dispatched.d.ts +2 -2
  72. package/dist/telegram/not-dispatched.d.ts.map +1 -1
  73. package/dist/telegram/outbound-budget.d.ts +53 -16
  74. package/dist/telegram/outbound-budget.d.ts.map +1 -1
  75. package/dist/telegram/outbound-cost.d.ts +5 -0
  76. package/dist/telegram/outbound-cost.d.ts.map +1 -0
  77. package/dist/telegram/outbound-pauses.d.ts +17 -0
  78. package/dist/telegram/outbound-pauses.d.ts.map +1 -0
  79. package/dist/telegram/outbound-queue.d.ts +17 -0
  80. package/dist/telegram/outbound-queue.d.ts.map +1 -0
  81. package/dist/telegram/update-intake-work.d.ts +20 -1
  82. package/dist/telegram/update-intake-work.d.ts.map +1 -1
  83. package/dist/telegram/update-intake.d.ts +12 -3
  84. package/dist/telegram/update-intake.d.ts.map +1 -1
  85. package/dist/telegram/update-store-check.d.ts +1 -1
  86. package/dist/telegram/update-store-check.d.ts.map +1 -1
  87. package/dist/telegram/update-store-postgres.d.ts.map +1 -1
  88. package/dist/telegram/update-store-sqlite.d.ts.map +1 -1
  89. package/dist/telegram/update-store.d.ts +5 -1
  90. package/dist/telegram/update-store.d.ts.map +1 -1
  91. package/dist/tracking/attribution.d.ts +1 -1
  92. package/dist/tracking/attribution.d.ts.map +1 -1
  93. package/dist/tracking/client-visit.d.ts +3 -1
  94. package/dist/tracking/client-visit.d.ts.map +1 -1
  95. package/dist/tracking/client.d.ts.map +1 -1
  96. package/dist/tracking/performance/collector.d.ts +61 -0
  97. package/dist/tracking/performance/collector.d.ts.map +1 -0
  98. package/dist/tracking/performance/schema.d.ts +45 -0
  99. package/dist/tracking/performance/schema.d.ts.map +1 -0
  100. package/dist/tracking/performance/summary.d.ts +12 -0
  101. package/dist/tracking/performance/summary.d.ts.map +1 -0
  102. package/dist/tracking/privacy.d.ts +5 -0
  103. package/dist/tracking/privacy.d.ts.map +1 -0
  104. package/dist/tracking/server/telemetry-health.d.ts +31 -0
  105. package/dist/tracking/server/telemetry-health.d.ts.map +1 -0
  106. package/dist/xlsx/cells.d.ts +17 -0
  107. package/dist/xlsx/cells.d.ts.map +1 -0
  108. package/dist/xlsx/render.d.ts +11 -0
  109. package/dist/xlsx/render.d.ts.map +1 -0
  110. package/dist/xlsx/schema.d.ts +119 -0
  111. package/dist/xlsx/schema.d.ts.map +1 -0
  112. package/dist/xlsx/zip.d.ts +13 -0
  113. package/dist/xlsx/zip.d.ts.map +1 -0
  114. package/llms/agent-runtime.2.txt +40 -16
  115. package/llms/agent-runtime.3.txt +17 -0
  116. package/llms/agent-runtime.txt +4 -1
  117. package/llms/primitives.txt +21 -0
  118. package/llms/stitchkit.txt +9 -6
  119. package/llms/telegram.txt +46 -15
  120. package/llms/tracking-contract.txt +32 -0
  121. package/llms/tracking-performance.txt +21 -0
  122. package/llms/tracking-server.txt +1 -0
  123. package/llms/tracking.txt +75 -12
  124. package/llms/{upgrading-0.105.0-0.116.0.txt → upgrading-0.105.0-0.118.0.txt} +73 -3
  125. package/llms/upgrading.txt +1 -1
  126. package/llms/xlsx.txt +107 -0
  127. package/llms-full.txt +392 -34
  128. package/llms.txt +11 -7
  129. package/package.json +27 -9
  130. package/dist/{index-z3d6smer.js → index-tdpmfks8.js} +5 -5
package/llms/telegram.txt CHANGED
@@ -131,17 +131,37 @@ cast, and a send cancelled in the queue then waits for its slot and fails as
131
131
  `messagesPerSecond`.
132
132
  - **The first send after idle is immediate; the rest are spaced.** This is a
133
133
  strict budget, not a bucket that saves a burst: at 24/s the next send is due
134
- one interval later. A 429 pauses the whole token; when it resumes, one send
135
- is released at the deadline and the queue continues one interval at a time,
136
- never as a stampede.
137
- - **It paces; it does not retry.** `run` calls the send once. A 429 pauses the
138
- token for Telegram's `retry_after` (or `retryAfterFallbackMs` when Telegram
139
- gave none) and rethrows; the caller's retry policy decides what happens next.
134
+ one interval later. When a pause ends, one send is released at the deadline and
135
+ the queue continues one interval at a time, never as a stampede.
136
+ - **A 429 holds the chat that got it.** Telegram words a group's limit, a chat's
137
+ pace and the bot's own limit alike, and only the addressee differs, so `run`
138
+ pauses the `chat_id` of the refused send and every other chat of the token goes
139
+ on at the shared rate. A person's reply does not wait behind a notification
140
+ group on its limit, and neither does a campaign. Refusals from
141
+ `tokenPauseChats` different chats (default 3) inside `tokenPauseWindowMs`
142
+ (default 5000) are read as the bot's own limit and pause the whole token, and so
143
+ does a 429 with no chat in the payload. `stats().pausedChats` counts the chats
144
+ on a pause; `stats().pausedForMs` is the token's.
145
+ - **A chat known to hit its limit can keep a 429 to itself.** A notification group
146
+ runs into its per-minute limit as a matter of course. `{ refusal: 'call' }` on `run`,
147
+ or `telegramBudgetTransformer(budget, { refusal: 'call' })` on that group's own `Api`,
148
+ makes the refusal concern that send alone: nothing pauses and nothing counts towards
149
+ `tokenPauseChats`. The call still spends the budget and waits out other pauses, and
150
+ what to do with the refused message stays the caller's decision.
151
+ - **A wait can be bounded.** A burst to one chat on pause otherwise queues without
152
+ limit. `{ maxWaitMs }` on `run` or `acquire`, or on
153
+ `telegramBudgetTransformer(budget, { maxWaitMs })`, ends a call that waited that
154
+ long with `TelegramNotDispatchedError`, stage `queue`: nothing was sent and no
155
+ message was spent, so repeating it is safe.
156
+ - **It paces; it does not retry.** `run` calls the send once. A 429 pauses for
157
+ Telegram's `retry_after` (or `retryAfterFallbackMs` when Telegram gave none) and
158
+ rethrows; the caller's retry policy decides what happens next.
140
159
  The refusal may be thrown or returned: a client transformer sits below the
141
160
  place where grammY turns Telegram's `{ ok: false, error_code: 429, … }` body into
142
161
  a `GrammyError`, so `run` reads that body too and hands it back unchanged.
143
- A 429 from an edit or a read concerns that call and pauses nothing; `budget.pauseAfter`
144
- remains for a caller who decides otherwise. A network failure is rethrown unchanged and stays `unknown`; a 5xx
162
+ A 429 from an edit or a read concerns that call and pauses nothing;
163
+ `budget.pauseAfter(error, { chatId })` remains for a caller who decides otherwise, and pauses
164
+ the token when it names no chat. A network failure is rethrown unchanged and stays `unknown`; a 5xx
145
165
  is rethrown unchanged and stays `server-error`. Neither is retried by the
146
166
  budget or pauses the token — repeating a send whose outcome is unknown is a
147
167
  decision, not a primitive's silent guess.
@@ -153,9 +173,9 @@ cast, and a send cancelled in the queue then waits for its slot and fails as
153
173
  the client's own error — not evidence that Telegram did not accept it — so
154
174
  keep the outcome unknown rather than retrying it as a definite failure.
155
175
  - **Non-send retries wait without spending.** An edit or delete retry that got
156
- a 429 first records the pause with `budget.pauseAfter(error)` (the `run` path
157
- does this itself), then uses `await budget.waitUntilResumed(signal)`; it shares
158
- the token's pause but does not take a message slot.
176
+ a 429 records the pause with `budget.pauseAfter(error, { chatId })`, then uses
177
+ `await budget.waitUntilResumed(signal, { chatId })`; it shares that chat's pause
178
+ and the token's, and takes no message slot. Without `chatId` it pauses the token.
159
179
 
160
180
  `telegramBroadcastSender` and `telegramOperatorSender` still send through
161
181
  `callTelegramBotApi`; they are not a second budget. Their returned function does
@@ -458,7 +478,17 @@ intake recorded the body; the handler runs after, outside the request. Updates
458
478
  of one chat run in order, other chats alongside (`maxConcurrent`, 32). An
459
479
  attempt holds a lease it renews while the handler lives; a sweep takes over what
460
480
  was left — `pending` whose process never got to it (`pendingGraceMs`), `failed`
461
- whose retry is due, `processing` whose process died. A failure is retried with
481
+ whose retry is due, `processing` whose process died. `start` schedules the whole
482
+ queue the last process left, page by page and without waiting for
483
+ `pendingGraceMs`, and an update accepted meanwhile takes its place behind it, so
484
+ a crash keeps each chat in order however long the queue was. The queue sits in
485
+ memory while it drains, as it did in the process that accepted it. `close`
486
+ waits for the handlers running and leaves the rest of the queue recorded. Order
487
+ holds inside one process for updates handled in their turn: the attempt a crash
488
+ interrupted comes when its lease lapses, a failed update at its retry and one
489
+ the store refused to claim at a later sweep, each after later updates of its
490
+ chat. An old release draining beside a new one shares its queue, so the two can
491
+ run updates of one chat side by side. A failure is retried with
462
492
  backoff or after Telegram's `retry_after`; one Telegram said repeating cannot
463
493
  fix (a refused message, a blocked user) is abandoned; `maxAttempts` (5) ends
464
494
  the rest. Delivery is at least once: a handler that finished but could not be
@@ -508,7 +538,8 @@ in a `BotError`; the intake unwraps it, so `retry`, `onFailure` and the error
508
538
  stored with the update are what the middleware threw — a `retry` that returns
509
539
  `false` for the bot's own terminal error sees that error.
510
540
 
511
- The base store remains the six atomic steps of `TelegramUpdateStore`. A
541
+ The base store remains the six atomic steps of `TelegramUpdateStore`; `due` is
542
+ paged, continuing strictly after `afterUpdateId`. A
512
543
  `TelegramUpdateFencedStore` adds `claimOwned`, `renewOwned`, `owns` and
513
544
  `settleOwned`; `TelegramUpdateDurableStore` adds the final-obligation methods.
514
545
  Reference stores implement both, so the bot's own database holds the fence and
@@ -615,7 +646,7 @@ injected grammY bot. → ADR 0143, ADR 0201, ADR 0274 — [guide](../guide/teleg
615
646
  | `TelegramFetch` / `TelegramBotTransportOpen` / `TelegramBotTransportResolve` / `TelegramBotTransportSocket` | _types_ | the `(url, init) => Promise<Response>` the senders take; the transport's socket factory, resolver and the structural socket it needs (a Node `net.Socket` or `tls.TLSSocket` fits) |
616
647
  | `TelegramNotDispatchedError` / `TelegramNotDispatchedStage` | class / _type_ | a request that provably never left (`lookup`, `connect` or `request`), so repeating it cannot duplicate a message |
617
648
  | `TelegramSendFailureReason` / `TELEGRAM_SEND_FAILURE_REASONS` | _type_ / constant | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `bot-to-bot-disabled` / `rate-limited` / `message-invalid` / `server-error` / `not-dispatched` / `unknown`, and the same as a list; a `Record<TelegramSendFailureReason, …>` over them fails to compile when a reason is added |
618
- | `telegramOutboundBudget` / `TelegramOutboundBudget` / `TelegramOutboundBudgetConfig` / `TelegramOutboundBudgetStats` / `TelegramOutboundCallOptions` / `TelegramOutboundLane` | function / _type_ | one token-wide outbound budget: one object per token in the process, strict 24 messages/s by default, weighted albums and multi-message copies, an `interactive` lane granted before `bulk`, `run(method, payload, signal, send, { lane })` as the transformer adapter (a 429 on a send pauses the token whether `send` throws it or resolves with the Bot API's `{ ok: false, … }` body), `pause`/`pauseAfter` for 429, `waitUntilResumed` for non-send retries and `stats`; it paces and never retries, and no public value carries the token |
649
+ | `telegramOutboundBudget` / `TelegramOutboundBudget` / `TelegramOutboundBudgetConfig` / `TelegramOutboundBudgetStats` / `TelegramOutboundCallOptions` / `TelegramOutboundChatOptions` / `TelegramOutboundLane` | function / _type_ | one token-wide outbound budget: one object per token in the process, strict 24 messages/s by default, weighted albums and multi-message copies, an `interactive` lane granted before `bulk`, `run(method, payload, signal, send, { lane, maxWaitMs })` as the transformer adapter (a 429 on a send pauses its `chat_id` whether `send` throws it or resolves with the Bot API's `{ ok: false, … }` body, and refusals from `tokenPauseChats` chats pause the token), `pause`/`pauseAfter` for 429, `waitUntilResumed` for non-send retries and `stats`; it paces and never retries, and no public value carries the token |
619
650
  | `runTelegramBroadcast` | function | a resumable broadcast by `name` under a state `directory`: audience and journal durably recorded, pacing at `ratePerSecond` (25), exact retry delays within the finite declared budget, injected classifier, uncertain send outcomes never replayed, `dryRun`, one fenced runner per name |
620
651
  | `TelegramBroadcastConfig` / `TelegramBroadcastSend` / `TelegramBroadcastReport` / `TelegramBroadcastRunOutcome` / `TelegramBroadcastOutcome` / `TelegramBroadcastRecipient` | _type_ | `name`, `directory`, `recipients()`, `send({ recipient, attempt })`, `maxAttempts`, `signal`, `onProgress`; counts `delivered` / `unreachable` / `failed` / `uncertain` / `pending` and `finished` / `stopped` / `halted` / `dry-run` |
621
652
  | `TelegramBroadcastFailureSchema` / `TelegramBroadcastFailure` | schema / _type_ | injected `classify` answers retry-after/transient/permanent/ambiguous; exact provider delay is bounded by `maxRetryDelayMs` without shortening, unknown outcome remains uncertain and is never replayed |
@@ -639,7 +670,7 @@ injected grammY bot. → ADR 0143, ADR 0201, ADR 0274 — [guide](../guide/teleg
639
670
  | `TelegramUpdateIntakeConfig` / `TelegramUpdateAttemptIntakeConfig` / `TelegramUpdateIntakeOptions` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, unchanged `handle(update)` or attempt-aware `handleAttempt(update, context)` (a grammY `BotError` is unwrapped), shared `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, durable `handleExhaustion`, `onExhaustionFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
640
671
  | `TelegramUpdateAttemptContext` / `TelegramUpdateAttemptIdentity` / `TelegramUpdateOwnershipLostError` | _type_ / class | immutable `{ updateId, attempt, claimId }` fence plus `ownerLost`; the persisted opaque claim token prevents a stale capability from matching after prune/re-add, while a store check in the consumer's DB transaction fences a domain write |
641
672
  | `TelegramUpdateExhaustion` / `TelegramUpdateExhaustionFailure` / `StoredTelegramUpdateExhaustion` / `TelegramUpdateExhaustionQuery` | _type_ | a terminal update/attempt/error retained in the update store until `handleExhaustion` resolves; ordered `{ limit, afterUpdateId? }` pages prevent a failing first batch from starving later rows; delivery failures are observational and leave it due |
642
- | `TelegramUpdateStore` / `StoredTelegramUpdate` / `TelegramUpdateState` / `TelegramUpdateSettlement` / `TelegramUpdateClaimOptions` / `TelegramUpdateDueQuery` | _type_ | the base store an intake runs on: `add`, `claim`, `renew`, `settle`, `due`, `prune`, each atomic on one record — a claim takes `pending`, a due `failed` or a lapsed `processing`; renew and settle only by the attempt holding it |
673
+ | `TelegramUpdateStore` / `StoredTelegramUpdate` / `TelegramUpdateState` / `TelegramUpdateSettlement` / `TelegramUpdateClaimOptions` / `TelegramUpdateDueQuery` | _type_ | the base store an intake runs on: `add`, `claim`, `renew`, `settle`, `due`, `prune`, each atomic on one record — a claim takes `pending`, a due `failed` or a lapsed `processing`; renew and settle only by the attempt holding it; `due` returns one page and continues strictly after `afterUpdateId` |
643
674
  | `TelegramUpdateFencedStore` / `TelegramUpdateDurableStore` | _type_ | reference-store capabilities: `claimOwned` creates and returns the persisted identity, `renewOwned`, `owns(identity, at)` and `settleOwned(identity, settlement)` act only on that exact unexpired claim; `exhaust`, `dueExhaustions`, `acknowledgeExhaustion` keep a final consumer obligation in the same row until exact acknowledgement |
644
675
  | `memoryTelegramUpdateStore` | function | the store in this process's memory — tests, or a bot that accepts losing unhandled updates to a restart |
645
676
  | `sqliteTelegramUpdateStore` / `SqliteTelegramUpdateStoreConfig` / `TelegramSqliteDatabase` | function / _type_ | the store in SQLite over the handle the application opened — `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync`; `table` (default `telegram_updates`) created when missing |
@@ -0,0 +1,32 @@
1
+ # stitchkit/tracking/contract — stitchkit docs slice
2
+
3
+ > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
+ > `llms.txt` in this package.
5
+
6
+ Contains: API reference: `stitchkit/tracking/contract` (docs/api/reference.md)
7
+
8
+ Also read:
9
+ - see `llms/tracking.txt` for the Visitor tracking guide
10
+
11
+ ==============================================================================
12
+ # API reference: `stitchkit/tracking/contract` (docs/api/reference.md)
13
+ ==============================================================================
14
+
15
+ ## `stitchkit/tracking/contract`
16
+
17
+ The wire shapes of tracking, for a server and a shared contract. It brings `zod`, so
18
+ browser code that only sends events does not import it. → ADR 0280
19
+
20
+ | Export | Kind | Summary |
21
+ |--------|------|---------|
22
+ | `createTrackingContract` / `TrackingContractConfig` / `TrackingContractEndpoints` | function / _type_ | `bootstrap` (`POST /visit`) and `track` (`POST /events`, `safelistedBody`) from the application's event types, scope and optional extras |
23
+ | `createTrackingSchemas` / `TrackingSchemas` / `TrackingSchemasConfig` | function / _type_ | every schema of one surface — `event`, `request`, `response`, `disposition`, `entry`, `bootstrap` — from `eventTypes`, optional `eventExtras`, `buildIdPattern`, `maxEventsPerBatch` |
24
+ | `TrackingEventEnvelope` / `TrackingEventShape` | _type_ | `eventId`, `visitId`, `browserStreamId`, `browserSequence`, `type`, `page`, `metadata`, `clientTimestamp` |
25
+ | `TrackEventsRequest` / `TrackEventsResponse` / `TrackEventsResponseSchema` | _type_ / schema | a batch (`buildId`, `events`, `utm`) and its answer (`accepted`, `dispositions`) |
26
+ | `TrackingDisposition` / `TrackingDispositionSchema` / `TrackingDispositionStatus` / `TrackingDispositionStatusSchema` | _type_ / schema | `accepted` / `duplicate` / `identity-invalid` / `excluded-bot` — every one terminal |
27
+ | `VisitEntryContext` / `VisitEntryContextShape` / `VisitBootstrapResponse` / `VisitBootstrapResponseSchema` | _type_ / schema | what a browser reports to open a visit (lineage, landing, referrer, UTM, display, screen, `buildId`, outbox health) and the lease it gets back |
28
+ | `TrackingOutboxState` / `TrackingOutboxStateSchema` | _type_ / schema | `available` / `unavailable` |
29
+ | `UtmData` / `UtmDataSchema` / `AttributionData` / `AttributionDataSchema` | _type_ / schema | campaign tags and a touch (UTM, referrer, landing page) |
30
+ | `DEFAULT_BUILD_ID_PATTERN` | const | `dev` or a git SHA |
31
+ | `PerformanceSampleSchema` / `PerformanceSample` / `PerformanceVitalsSchema` / `PerformanceVitals` | schemas / _types_ | one page load's wire shape; a metric that did not arrive is `null` |
32
+ | `summarizePerformanceMetric` / `PerformanceMetricSummary` | function / _type_ | sample count, p75, and a p95 only from 100 samples |
@@ -0,0 +1,21 @@
1
+ # stitchkit/tracking/performance — stitchkit docs slice
2
+
3
+ > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
+ > `llms.txt` in this package.
5
+
6
+ Contains: API reference: `stitchkit/tracking/performance` (docs/api/reference.md)
7
+
8
+ Also read:
9
+ - see `llms/tracking.txt` for the Visitor tracking guide
10
+
11
+ ==============================================================================
12
+ # API reference: `stitchkit/tracking/performance` (docs/api/reference.md)
13
+ ==============================================================================
14
+
15
+ ## `stitchkit/tracking/performance`
16
+
17
+ | Export | Kind | Summary |
18
+ |--------|------|---------|
19
+ | `createPerformanceCollector` / `PerformanceCollector` / `PerformanceCollectorConfig` | function / _types_ | Web Vitals of this page after load: an `early` sample and a `final` beacon to `url`, undelivered ones kept for the next page; inert for a lab agent |
20
+ | `PerformanceHost` / `VitalsLibrary` / `VitalsMetric` / `VitalsObserver` | _types_ | the seams of the collector: the page, and the part of `web-vitals` it uses |
21
+ | `isLabUserAgent` | function | whether an agent admits to being a lab run or a crawler |
@@ -27,6 +27,7 @@ algorithm over a store the application implements. → ADR 0166
27
27
  | `activeIntervalOf` / `ActiveTimeInterval` / `ActiveIntervalOptions` | function / _type_ | the visible-time interval an event carries, dated by the server clock; `null` for an unshaped or implausible one |
28
28
  | `hashTrackingEvent` | function | `sha256(JSON.stringify(parsedEvent))` — the stored payload hash, unsorted on purpose so it matches what applications already hold |
29
29
  | `isBotUserAgent` / `DEFAULT_BOT_USER_AGENT_PATTERN` | function / const | crawlers, monitors and `headlesschrome` are not visitors |
30
+ | `trackingTelemetryHealth` / `TrackingOutboxReport` / `TrackingTelemetryHealth` | function / _types_ | `ok`, `degraded` or `unknown` from outbox reports; a lifetime `dropped` counts only by what it grew over `baselines` |
30
31
  | `createPresenceRegistry` / `PresenceRegistry` / `PresenceEntry` | function / _type_ | who is here now, in this process: `touch` / `snapshot(filter)` / `presentVisitOf` with a 45 s TTL |
31
32
 
32
33
  ---
package/llms/tracking.txt CHANGED
@@ -33,7 +33,7 @@ Both entrypoints are **evolving**.
33
33
  ## Contract
34
34
 
35
35
  ```ts
36
- import { createTrackingContract } from 'stitchkit/tracking'
36
+ import { createTrackingContract } from 'stitchkit/tracking/contract'
37
37
 
38
38
  export const tracking = createTrackingContract({
39
39
  scope: 'public',
@@ -232,6 +232,76 @@ has an identity. A bot gets a lease that no store holds, so its events are
232
232
  `createPresenceRegistry` is who is here now, in this process: touch it from
233
233
  `track`, read it for a live feed. It is honestly empty after a restart.
234
234
 
235
+ ## Real-visitor speed
236
+
237
+ `stitchkit/tracking/performance` collects Web Vitals from the people who use the site. Install the
238
+ optional peer `web-vitals` and call it once on the page:
239
+
240
+ ```ts
241
+ import { createPerformanceCollector } from 'stitchkit/tracking/performance'
242
+
243
+ createPerformanceCollector({
244
+ buildId: BUILD_ID,
245
+ url: '/api/tracking/performance',
246
+ visitId: () => client.visitId(),
247
+ })
248
+ ```
249
+
250
+ The library is fetched as its own chunk after `load`, when the browser is idle, so the measurement costs
251
+ the page nothing it is measuring. Each load posts an `early` sample five seconds later and a `final` one
252
+ as the page goes, both with `sendBeacon` and the same `id`: keep the later one. The body is a string, so
253
+ the endpoint declares `safelistedBody: true`. A sample that could not leave waits in session storage for the
254
+ next page, up to 20 of them, and of one load only the newest.
255
+
256
+ A browser under automation sends nothing, and neither does an agent that admits to being Lighthouse,
257
+ headless Chrome, GTmetrix or a crawler. Lighthouse with its default settings imitates an ordinary browser, so a
258
+ local run with `webdriver` off cannot be told from a visitor.
259
+
260
+ One load is one document: a client-side navigation stays under the route the page opened on, and whatever
261
+ `web-vitals` reports after a back-forward restore is left out, because it would replace the load's numbers
262
+ with a restore's. A second call on a page returns the running collector.
263
+
264
+ A sample carries the route, the build, `mobile` or `desktop`, and the five metrics. **A metric that did not
265
+ arrive is `null`, never zero**: most visits have no INP at all, and a zero would pull every average down.
266
+
267
+ Parse a sample on your own endpoint and store it in your own table:
268
+
269
+ ```ts
270
+ import { redactTrackingText } from 'stitchkit/tracking'
271
+ import { PerformanceSampleSchema, summarizePerformanceMetric } from 'stitchkit/tracking/contract'
272
+
273
+ const sample = PerformanceSampleSchema.parse(await request.json())
274
+ await samples.upsert({ ...sample, route: redactTrackingText(sample.route) })
275
+
276
+ const lcp = summarizePerformanceMetric(rows.map((row) => row.vitals.lcpMs))
277
+ // { samples: 42, p75: 1840, p95: null } — a p95 needs 100 samples
278
+ ```
279
+
280
+ The report is yours: which routes matter and what is slow are the application's words.
281
+
282
+ ## Privacy
283
+
284
+ `redactTrackingText` masks passwords, sign-in credentials, API keys, bearer tokens and signed Telegram launch
285
+ data in a URL or free text, and leaves paths and campaign parameters byte for byte. Text past 16 KiB is cut
286
+ first, because the endpoint is open to anyone. `code` and `hash` count only beside the
287
+ `state` or `auth_date` that makes them credentials, so a promo code stays. `redactTrackingValue` applies the
288
+ same rule through a value of any shape. The client applies it to the landing path, the referrer and the
289
+ campaign tags before sending or storing them. Apply it again on your ingest: anyone can post to that endpoint without running the client.
290
+
291
+ ## Is tracking losing events?
292
+
293
+ `trackingTelemetryHealth(reports, baselines)` reads the outbox figures of a visit's bootstrap
294
+ (`browserStreamId` as `stream`, `outboxState`, `outboxQueued`, `outboxDropped`) and answers `ok`, `degraded` or
295
+ `unknown`. An application running without an outbox reports `unavailable` and reads as `degraded`. A browser repeats its lifetime `dropped` with
296
+ every visit, so pass what each stream reported before the window as `baselines`, and only the growth counts.
297
+
298
+ ## Bundle weight
299
+
300
+ `stitchkit/tracking` has no `zod` in its graph: `createTrackingClient` with `indexedDbOutboxStorage` is under
301
+ 8 KB gzip, a test holds that, and the build refuses a published file that reaches `zod`. The schemas live in
302
+ `stitchkit/tracking/contract` for the server and a shared contract. Importing that in code a browser loads
303
+ brings `zod` back, so keep a shared contract out of the storefront's client bundle.
304
+
235
305
  ## Not here
236
306
 
237
307
  GeoIP, event labels, funnels, reports, the analytics page, Socket.IO push into
@@ -246,8 +316,9 @@ looks different even though their mechanics are now one.
246
316
 
247
317
  Browser-safe visitor-tracking mechanics, with no event vocabulary of their own
248
318
  and no React. The React wrapper (a provider and a hook, ~40 lines) is the
249
- application's — it owns the router and the session. The server half is
250
- `stitchkit/tracking/server`. → ADR 0166 — [guide](../guide/tracking.md)
319
+ application's — it owns the router and the session. It imports no `zod`: the wire
320
+ shapes are `stitchkit/tracking/contract`, the server half is
321
+ `stitchkit/tracking/server`. → ADR 0166, ADR 0280 — [guide](../guide/tracking.md)
251
322
 
252
323
  | Export | Kind | Summary |
253
324
  |--------|------|---------|
@@ -256,15 +327,6 @@ application's — it owns the router and the session. The server half is
256
327
  | `TrackFn` / `EventsWithMetadata` / `EventsWithoutMetadata` | _type_ | `track(type)` / `track(type, metadata)` typed by the application's metadata map, and the two halves of that map |
257
328
  | `BuiltinTrackingEventTypes` / `CONVENTIONAL_TRACKING_EVENT_TYPES` | _type_ / const | the application's names for the events the client emits itself; the conventional `PAGE_VIEW` … `INTERACTION` set |
258
329
  | `browserTrackingHost` / `TrackingHost` / `TrackingPageContext` | function / _type_ | everything the client takes from a browser tab behind one interface — page context, visibility, scroll depth, events, intervals, clocks, storage — so a test or a non-tab host can supply its own |
259
- | `createTrackingContract` / `TrackingContractConfig` / `TrackingContractEndpoints` | function / _type_ | `bootstrap` (`POST /visit`) and `track` (`POST /events`, `safelistedBody`) from the application's event types, scope and optional extras |
260
- | `createTrackingSchemas` / `TrackingSchemas` / `TrackingSchemasConfig` | function / _type_ | every schema of one surface — `event`, `request`, `response`, `disposition`, `entry`, `bootstrap` — from `eventTypes`, optional `eventExtras`, `buildIdPattern`, `maxEventsPerBatch` |
261
- | `TrackingEventEnvelope` / `TrackingEventShape` | _type_ | `eventId`, `visitId`, `browserStreamId`, `browserSequence`, `type`, `page`, `metadata`, `clientTimestamp` |
262
- | `TrackEventsRequest` / `TrackEventsResponse` / `TrackEventsResponseSchema` | _type_ / schema | a batch (`buildId`, `events`, `utm`) and its answer (`accepted`, `dispositions`) |
263
- | `TrackingDisposition` / `TrackingDispositionSchema` / `TrackingDispositionStatus` / `TrackingDispositionStatusSchema` | _type_ / schema | `accepted` / `duplicate` / `identity-invalid` / `excluded-bot` — every one terminal |
264
- | `VisitEntryContext` / `VisitEntryContextShape` / `VisitBootstrapResponse` / `VisitBootstrapResponseSchema` | _type_ / schema | what a browser reports to open a visit (lineage, landing, referrer, UTM, display, screen, `buildId`, outbox health) and the lease it gets back |
265
- | `TrackingOutboxState` / `TrackingOutboxStateSchema` | _type_ / schema | `available` / `unavailable` |
266
- | `UtmData` / `UtmDataSchema` / `AttributionData` / `AttributionDataSchema` | _type_ / schema | campaign tags and a touch (UTM, referrer, landing page) |
267
- | `DEFAULT_BUILD_ID_PATTERN` | const | `dev` or a git SHA |
268
330
  | `createTrackingOutbox` / `TrackingOutbox` / `TrackingOutboxOptions` / `TrackingOutboxHealth` | function / _type_ | the tab-shared outbox: `streamId`, `reserveSequences`, `enqueue`, `acquireLease` / `releaseLease` (short, released on leave), `readBatch`, `acknowledge`, `health`; bounded by count and age |
269
331
  | `TrackingOutboxStorage` / `TrackingOutboxMeta` / `TrackingOutboxEvents` / `TrackingOutboxRecord` / `TrackingQueuedEvent` | _type_ | one atomic, all-or-nothing `transact` over a key/value store and an event store (`readonly` for pure reads) — the contract an adapter implements |
270
332
  | `indexedDbOutboxStorage` / `memoryOutboxStorage` | function | the IndexedDB adapter (one `readwrite` transaction per `transact`) and the in-memory one for tests and degraded mode |
@@ -276,6 +338,7 @@ application's — it owns the router and the session. The server half is
276
338
  | `createOncePerPage` / `OncePerPage` | function / _type_ | one fact per key per page within a window — the guard a twice-mounted component cannot keep itself |
277
339
  | `resolveAttribution` / `ResolveAttributionInput` / `ResolvedAttribution` / `AttributionStorage` / `ReferrerRule` | function / _type_ | first-touch once, current-touch on every new UTM; the referrer → source/medium map is the application's, there is no built-in list |
278
340
  | `parseUtmFromSearch` / `parseReferrer` | function | the two parsers `resolveAttribution` is built from |
341
+ | `redactTrackingText` / `redactTrackingValue` | functions | mask passwords, sign-in credentials and signed launch data in text or a value of any shape; the key stays, and navigation and campaign parameters are untouched |
279
342
  | `resolveTrackedClick` / `ResolveTrackedClickOptions` / `TrackedClick` / `TrackedClickAttributes` / `ClickTarget` | function / _type_ | `data-track` → click, `data-track-action` → interaction, a link to another origin → outbound; attribute names configurable; `hrefPolicy: 'full'` retains query/fragment (default `path-only` omits them), URL credentials excluded; `leavesPage` says which path to send on |
280
343
 
281
344
  ---
@@ -1,14 +1,84 @@
1
- # Upgrading 0.105.0 → 0.116.0 — stitchkit docs slice
1
+ # Upgrading 0.105.0 → 0.118.0 — stitchkit docs slice
2
2
 
3
3
  > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
4
  > `llms.txt` in this package.
5
5
 
6
- Contains: Guide: Upgrading, 0.105.0 → 0.116.0 (docs/guide/upgrading.md)
6
+ Contains: Guide: Upgrading, 0.105.0 → 0.118.0 (docs/guide/upgrading.md)
7
7
 
8
8
  ==============================================================================
9
- # Guide: Upgrading, 0.105.0 → 0.116.0 (docs/guide/upgrading.md)
9
+ # Guide: Upgrading, 0.105.0 → 0.118.0 (docs/guide/upgrading.md)
10
10
  ==============================================================================
11
11
 
12
+ ## Released migration: 0.118.0
13
+
14
+ ### telegram-due-cursor
15
+
16
+ **Who must act:** an application with its own `TelegramUpdateStore`. A bot on `memoryTelegramUpdateStore`,
17
+ `sqliteTelegramUpdateStore` or `postgresTelegramUpdateStore` needs no change.
18
+
19
+ `due` returns one page of a queue whose rows stay due until claimed, and the intake reads the next page from
20
+ `afterUpdateId`, until a page comes back empty. A store that ignores the cursor fails `start` and every sweep
21
+ with a `TypeError`, reported to `onStoreError`, as soon as a row is due. Continue strictly after it, in
22
+ `update_id` order:
23
+
24
+ ```ts
25
+ // before
26
+ where: { OR: dueRows }
27
+ // after
28
+ where: { OR: dueRows, ...(query.afterUpdateId !== undefined && { updateId: { gt: query.afterUpdateId } }) }
29
+ ```
30
+
31
+ `checkTelegramUpdateStore` reports the rule as `due continues strictly after its cursor`.
32
+
33
+ Search pattern: an object with a `due(` method passed as `store` to `createTelegramUpdateIntake`.
34
+
35
+ ## Released migration: 0.117.0
36
+
37
+ ### telegram-chat-pause
38
+
39
+ **Who must act:** code that relied on a 429 to one chat to stop the token's other chats. A bot that
40
+ only sends needs no change: its other chats stop waiting.
41
+
42
+ `run` pauses the `chat_id` of a refused send; the token pauses when `tokenPauseChats` chats are refused
43
+ inside `tokenPauseWindowMs`, or when the call has no chat
44
+ ([ADR 0278](../decisions/0278-a-telegram-429-holds-the-chat-that-got-it.md)). A second budget kept under a
45
+ derived key to isolate one group is no longer needed: remove it. Where one refusal must still stop everything,
46
+ say so:
47
+
48
+ ```ts
49
+ // before
50
+ await api.sendMessage(chatId, text)
51
+ // after
52
+ await api.sendMessage(chatId, text).catch((error) => {
53
+ budget.pauseAfter(error)
54
+ throw error
55
+ })
56
+ ```
57
+
58
+ The catch belongs at the `Api` call, where grammY has turned a returned refusal into a thrown one. A raw
59
+ `budget.run` around a client transformer's `send` gets that refusal as a value.
60
+
61
+ Search pattern: `budget.run`, and budgets created under a key derived from the token.
62
+
63
+ ### tracking-contract-entry
64
+
65
+ **Who must act:** code that imports `createTrackingContract` or a tracking schema or wire type from
66
+ `stitchkit/tracking`. Code that only uses the browser client needs no change.
67
+
68
+ The wire shapes moved to `stitchkit/tracking/contract`, so a page that sends events no longer carries `zod`
69
+ ([ADR 0280](../decisions/0280-real-visitor-speed-is-collected-after-the-page.md)):
70
+
71
+ ```ts
72
+ // before
73
+ import { createTrackingContract, createTrackingSchemas } from 'stitchkit/tracking'
74
+ // after
75
+ import { createTrackingContract, createTrackingSchemas } from 'stitchkit/tracking/contract'
76
+ ```
77
+
78
+ Search pattern: imports from `stitchkit/tracking` naming `createTrackingContract`, `createTrackingSchemas`,
79
+ `UtmData`, `AttributionData`, `TrackingEventEnvelope`, `TrackingDisposition`, `TrackEvents*`, `VisitEntryContext`,
80
+ `VisitBootstrapResponse`, `TrackingOutboxState` or `DEFAULT_BUILD_ID_PATTERN`.
81
+
12
82
  ## Released migration: 0.116.0
13
83
 
14
84
  ### send-only-telegram-pause
@@ -11,7 +11,7 @@ Contains: Guide: Upgrading, general (docs/guide/upgrading.md)
11
11
 
12
12
  Load this slice, then every range slice between your installed version and the target:
13
13
 
14
- - 0.105.0 → 0.116.0: `llms/upgrading-0.105.0-0.116.0.txt`
14
+ - 0.105.0 → 0.118.0: `llms/upgrading-0.105.0-0.118.0.txt`
15
15
  - 0.84.0 → 0.104.0: `llms/upgrading-0.84.0-0.104.0.txt`
16
16
  - 0.63.0 → 0.83.0: `llms/upgrading-0.63.0-0.83.0.txt`
17
17
  - 0.44.0 → 0.62.0: `llms/upgrading-0.44.0-0.62.0.txt`
package/llms/xlsx.txt ADDED
@@ -0,0 +1,107 @@
1
+ # stitchkit/xlsx — stitchkit docs slice
2
+
3
+ > Generated from the `docs/` tree by `bun run gen:llms`. The index of all slices is
4
+ > `llms.txt` in this package.
5
+
6
+ Contains: Guide: XLSX (docs/guide/xlsx.md) · API reference: `stitchkit/xlsx` (docs/api/reference.md)
7
+
8
+ ==============================================================================
9
+ # Guide: XLSX (docs/guide/xlsx.md)
10
+ ==============================================================================
11
+
12
+ # XLSX
13
+
14
+ `stitchkit/xlsx` turns rows into a workbook. It is evolving, it needs nothing beyond the `zod` the package
15
+ already requires, and it runs wherever
16
+ `CompressionStream` does: Bun, Node and a browser.
17
+
18
+ ```ts
19
+ import { defineMoney } from 'stitchkit/primitives'
20
+ import { renderXlsx, XLSX_MEDIA_TYPE } from 'stitchkit/xlsx'
21
+
22
+ const rub = defineMoney('RUB')
23
+
24
+ const bytes = await renderXlsx({
25
+ sheets: [
26
+ {
27
+ name: 'Orders',
28
+ columns: [
29
+ { key: 'title', header: 'Title', type: 'text' },
30
+ { key: 'count', header: 'Count', type: 'number', format: '0.00' },
31
+ { key: 'day', header: 'Day', type: 'date' },
32
+ { key: 'at', header: 'Created', type: 'datetime', timeZone: 'Asia/Bangkok' },
33
+ { key: 'total', header: 'Total', type: 'money', currency: 'RUB', scale: 2 },
34
+ { key: 'paid', header: 'Paid', type: 'boolean' },
35
+ ],
36
+ rows: [
37
+ {
38
+ title: 'First order',
39
+ count: '2.5',
40
+ day: '2026-11-03',
41
+ at: new Date(),
42
+ total: rub.create('504182'),
43
+ paid: true,
44
+ },
45
+ ],
46
+ },
47
+ ],
48
+ })
49
+
50
+ return new Response(bytes, {
51
+ headers: {
52
+ 'content-type': XLSX_MEDIA_TYPE,
53
+ 'content-disposition': `attachment; filename*=UTF-8''${encodeURIComponent('Übersicht.xlsx')}`,
54
+ },
55
+ })
56
+ ```
57
+
58
+ A missing or `null` value leaves the cell empty. The first row holds the headers, stays frozen and carries a
59
+ filter. The same input produces the same bytes on one runtime; Bun and Node compress differently.
60
+
61
+ ## Column types
62
+
63
+ | `type` | Cell value | In the file |
64
+ |---|---|---|
65
+ | `text` | string | text, whatever it looks like: `007` keeps its zeros |
66
+ | `number` | number, bigint or decimal string | a number, with an optional Excel `format` |
67
+ | `date` | `YYYY-MM-DD` | a date serial; a calendar date has no time zone to get wrong |
68
+ | `datetime` | `Date` or ISO instant with an offset | the wall clock of the column's `timeZone` to the second, since a cell stores no zone |
69
+ | `money` | `Money` in the column's `currency` | a number with `scale` places and the currency in its format |
70
+ | `boolean` | boolean | a boolean |
71
+
72
+ ## What it refuses
73
+
74
+ A value of the wrong type throws `XlsxCellError` naming the cell, such as `sheet "Orders" B2: expected a
75
+ number or a decimal string`, instead of landing as a wrong cell.
76
+
77
+ Excel keeps numbers as doubles, so 15 significant digits are what survives. A decimal string or `Money` with
78
+ more refuses rather than turning into a different amount. Keep such a value in a `text` column if it must
79
+ stay exact and needs no arithmetic. Dates before 1900-03-01 refuse too: Excel counts a leap day that never
80
+ happened, so its serials before it are off by one.
81
+
82
+ A wall clock cannot tell the two hours of a daylight-saving change apart: 05:30Z and 06:30Z on a New York
83
+ autumn night are the same cell. Use `UTC` for a column whose order must survive that hour.
84
+
85
+ ## Text is never a formula
86
+
87
+ Cells are written as inline strings, so `=HYPERLINK("x")` from a user's input is text a reader shows,
88
+ and the file has no formula element at all. A cell that starts with `=`, `+`, `-` or `@` also carries Excel's
89
+ quote prefix, so it stays text when a person edits that cell.
90
+
91
+ ## Limits
92
+
93
+ Everything is built in memory, up to Excel's own 1 048 576 rows per sheet. There is no reading, no
94
+ charts, no merged cells and no streaming: an application that needs them takes a library.
95
+
96
+ ==============================================================================
97
+ # API reference: `stitchkit/xlsx` (docs/api/reference.md)
98
+ ==============================================================================
99
+
100
+ ## `stitchkit/xlsx`
101
+
102
+ | Export | Kind | Summary |
103
+ |--------|------|---------|
104
+ | `renderXlsx` | function | a workbook from rows and typed columns as XLSX bytes; deterministic, no formula in the file |
105
+ | `XLSX_MEDIA_TYPE` | constant | the media type for `defineExportOperation` and a download response |
106
+ | `XlsxCellError` | class | a value of the wrong type, or one Excel cannot hold exactly, with the cell's address |
107
+ | `XlsxWorkbookSchema` / `XlsxWorkbook` / `XlsxSheetSchema` / `XlsxColumnSchema` / `XlsxColumn` | schemas / _types_ | the workbook, its sheets and the six column types |