modelpact 2.0.2
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.
- package/LICENSE +21 -0
- package/README.md +372 -0
- package/dist/helpers/abort.d.ts +14 -0
- package/dist/helpers/abort.d.ts.map +1 -0
- package/dist/helpers/abort.js +22 -0
- package/dist/helpers/abort.js.map +1 -0
- package/dist/helpers/lifetime.d.ts +34 -0
- package/dist/helpers/lifetime.d.ts.map +1 -0
- package/dist/helpers/lifetime.js +71 -0
- package/dist/helpers/lifetime.js.map +1 -0
- package/dist/helpers/monitor.d.ts +52 -0
- package/dist/helpers/monitor.d.ts.map +1 -0
- package/dist/helpers/monitor.js +69 -0
- package/dist/helpers/monitor.js.map +1 -0
- package/dist/helpers/ndjson.d.ts +19 -0
- package/dist/helpers/ndjson.d.ts.map +1 -0
- package/dist/helpers/ndjson.js +39 -0
- package/dist/helpers/ndjson.js.map +1 -0
- package/dist/helpers/overflow.d.ts +50 -0
- package/dist/helpers/overflow.d.ts.map +1 -0
- package/dist/helpers/overflow.js +72 -0
- package/dist/helpers/overflow.js.map +1 -0
- package/dist/helpers/transcript.d.ts +22 -0
- package/dist/helpers/transcript.d.ts.map +1 -0
- package/dist/helpers/transcript.js +26 -0
- package/dist/helpers/transcript.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/01_access.d.ts +13 -0
- package/dist/lifecycle/01_access.d.ts.map +1 -0
- package/dist/lifecycle/01_access.js +45 -0
- package/dist/lifecycle/01_access.js.map +1 -0
- package/dist/lifecycle/02_download.d.ts +8 -0
- package/dist/lifecycle/02_download.d.ts.map +1 -0
- package/dist/lifecycle/02_download.js +12 -0
- package/dist/lifecycle/02_download.js.map +1 -0
- package/dist/lifecycle/03_open.d.ts +22 -0
- package/dist/lifecycle/03_open.d.ts.map +1 -0
- package/dist/lifecycle/03_open.js +39 -0
- package/dist/lifecycle/03_open.js.map +1 -0
- package/dist/lifecycle/04_generate.d.ts +15 -0
- package/dist/lifecycle/04_generate.d.ts.map +1 -0
- package/dist/lifecycle/04_generate.js +154 -0
- package/dist/lifecycle/04_generate.js.map +1 -0
- package/dist/lifecycle/05_close.d.ts +4 -0
- package/dist/lifecycle/05_close.d.ts.map +1 -0
- package/dist/lifecycle/05_close.js +8 -0
- package/dist/lifecycle/05_close.js.map +1 -0
- package/dist/providers/create.d.ts +5 -0
- package/dist/providers/create.d.ts.map +1 -0
- package/dist/providers/create.js +7 -0
- package/dist/providers/create.js.map +1 -0
- package/dist/providers/mock.d.ts +36 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +128 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/providers/ollama.d.ts +33 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +275 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/prompt-api.d.ts +23 -0
- package/dist/providers/prompt-api.d.ts.map +1 -0
- package/dist/providers/prompt-api.js +203 -0
- package/dist/providers/prompt-api.js.map +1 -0
- package/dist/providers/registry.d.ts +22 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +30 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/testing/contract.d.ts +47 -0
- package/dist/testing/contract.d.ts.map +1 -0
- package/dist/testing/contract.js +518 -0
- package/dist/testing/contract.js.map +1 -0
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types/backend.d.ts +63 -0
- package/dist/types/backend.d.ts.map +1 -0
- package/dist/types/backend.js +11 -0
- package/dist/types/backend.js.map +1 -0
- package/dist/types/failures.d.ts +115 -0
- package/dist/types/failures.d.ts.map +1 -0
- package/dist/types/failures.js +68 -0
- package/dist/types/failures.js.map +1 -0
- package/dist/types/foundations.d.ts +35 -0
- package/dist/types/foundations.d.ts.map +1 -0
- package/dist/types/foundations.js +30 -0
- package/dist/types/foundations.js.map +1 -0
- package/dist/types/messages.d.ts +30 -0
- package/dist/types/messages.d.ts.map +1 -0
- package/dist/types/messages.js +2 -0
- package/dist/types/messages.js.map +1 -0
- package/dist/types/provider.d.ts +22 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/provider.js +2 -0
- package/dist/types/provider.js.map +1 -0
- package/dist/types/session.d.ts +156 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +2 -0
- package/dist/types/session.js.map +1 -0
- package/dist/types/usage.d.ts +25 -0
- package/dist/types/usage.d.ts.map +1 -0
- package/dist/types/usage.js +16 -0
- package/dist/types/usage.js.map +1 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergey Avdienko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# modelpact
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/modelpact)
|
|
4
|
+
[](https://github.com/AvdienkoSergey/modelpact/actions/workflows/ci.yml)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
**One way to talk to a local language model — the one built into the browser,
|
|
12
|
+
Ollama on your machine, or a mock in your tests. Swap the backend, keep the
|
|
13
|
+
code.**
|
|
14
|
+
|
|
15
|
+
## Why you'd want it
|
|
16
|
+
|
|
17
|
+
Chrome now ships a language model inside the browser. It runs on the user's
|
|
18
|
+
device: free, offline, private, no API key. For a web app that is the best deal
|
|
19
|
+
in AI — as long as your users are on Chrome.
|
|
20
|
+
|
|
21
|
+
Everyone else needs a fallback, and every fallback speaks its own dialect. A
|
|
22
|
+
different way to ask "are you available?", a different download story, different
|
|
23
|
+
errors, a different streaming format. Support two backends and you maintain two
|
|
24
|
+
integrations that share nothing.
|
|
25
|
+
|
|
26
|
+
modelpact is one dialect for all of them.
|
|
27
|
+
|
|
28
|
+
## What you get
|
|
29
|
+
|
|
30
|
+
**Swap backends with one line.** The provider is the only place a backend is
|
|
31
|
+
named. Everything after it is identical.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const provider = makePromptApiProvider(); // Chrome's built-in model
|
|
35
|
+
const provider = makeOllamaProvider({ model: "granite4:350m" }); // …or a daemon
|
|
36
|
+
const provider = makeMockProvider(); // …or nothing at all, in tests
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Failures that say what to do next.** No parsing exception names. Each failure
|
|
40
|
+
is a plain word with the data you need to act on it: `context-overflow` comes
|
|
41
|
+
with the usage so you can trim, `busy` tells you which call holds the session,
|
|
42
|
+
`unsupported-input` says what was wrong with your message.
|
|
43
|
+
|
|
44
|
+
**Bugs that don't compile.** You cannot open a session on a model that isn't
|
|
45
|
+
there — the method doesn't exist on that branch. You cannot read an answer
|
|
46
|
+
without handling the failure — the field isn't in the type. TypeScript catches
|
|
47
|
+
it before your users do.
|
|
48
|
+
|
|
49
|
+
**A conversation you can carry across a reload.** The session keeps the record:
|
|
50
|
+
what you handed it, then every completed turn. It lives in memory for one tab,
|
|
51
|
+
so storing it is yours — `localStorage`, IndexedDB, your server. Read it, store
|
|
52
|
+
it, hand it back to `open`, and the conversation continues where it was. An
|
|
53
|
+
aborted turn never gets in, so what you store is what the model actually saw.
|
|
54
|
+
|
|
55
|
+
**Download progress for free.** The first time a browser model is used, hundreds
|
|
56
|
+
of megabytes move. You get a progress event; your users get a bar instead of a
|
|
57
|
+
frozen page.
|
|
58
|
+
|
|
59
|
+
**Tests that need no GPU.** The mock provider is a first-class backend. Your
|
|
60
|
+
suite runs on any CI box.
|
|
61
|
+
|
|
62
|
+
**Bring your own backend and prove it.** Wrote an adapter for something else?
|
|
63
|
+
Run it through the same conformance suite the built-in providers pass. If it's
|
|
64
|
+
green, it behaves like the others — not "probably", provably.
|
|
65
|
+
|
|
66
|
+
## See it before you install it
|
|
67
|
+
|
|
68
|
+
[`demo/`](demo/) is one chat screen with a picker at the top. The picker holds
|
|
69
|
+
six backends, and each of them was one line in
|
|
70
|
+
[`demo/src/providers.ts`](demo/src/providers.ts) — nothing else on the screen
|
|
71
|
+
knows which one is answering.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
cd demo && npm install && npm run dev
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Pick | What answers |
|
|
78
|
+
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
79
|
+
| `mock`, `mock-narrow`, `mock-download` | nothing: canned words, streamed one at a time, to stage every branch |
|
|
80
|
+
| `ollama` | `granite4:350m` — 708 MB on your machine, offline once it is pulled |
|
|
81
|
+
| `prompt-api` | Chrome's built-in model, behind a consent button for its download |
|
|
82
|
+
| `webgpu` | `SmolLM2-360M` in the tab itself, from a package outside this repo |
|
|
83
|
+
|
|
84
|
+
<p align="center">
|
|
85
|
+
<img src="docs/screenshots/demo-overflow.png" width="49%" alt="The mock on a 60-token window: three turns in, the meter reads 140 / 60 and the overflow notice has fired once.">
|
|
86
|
+
<img src="docs/screenshots/demo-chrome.png" width="49%" alt="Chrome's built-in model with no weights on this machine yet: the chip says fetching weights, and the download waits for the button.">
|
|
87
|
+
</p>
|
|
88
|
+
|
|
89
|
+
Everything on that screen is one of the promises above: words arriving one at a
|
|
90
|
+
time, a **Stop** that leaves the session open, the interrupted answer gone from
|
|
91
|
+
the record, a meter, a chip per `AccessKind`, an overflow warning that fires
|
|
92
|
+
once, a download bar that waits to be asked, and a conversation that survives a
|
|
93
|
+
reload and stays in step across two tabs. Nine Playwright specs — one per
|
|
94
|
+
promise, and one per real backend in the picker — drive it in Chromium and are
|
|
95
|
+
the repo's whole browser suite.
|
|
96
|
+
|
|
97
|
+
The last three rows are the point. A real model on a daemon, the browser's own
|
|
98
|
+
model, and a model on WebGPU are not three integrations here. They are three
|
|
99
|
+
entries in a registry, and the switch over that registry stays exhaustive — add
|
|
100
|
+
a seventh and the build says where a sentence is missing.
|
|
101
|
+
|
|
102
|
+
## The step, and what stands on it
|
|
103
|
+
|
|
104
|
+
Talking to a model has three storeys. A transport reaches **one model**. A
|
|
105
|
+
session holds **one conversation** with it and carries the guarantees — one
|
|
106
|
+
generation at a time, an abort that leaves the session open, an overflow that
|
|
107
|
+
fires once, a close that refuses everything after it. Above that, several
|
|
108
|
+
models, a policy, a loop over turns: orchestration.
|
|
109
|
+
|
|
110
|
+
modelpact is the middle storey, and it is deliberately only that.
|
|
111
|
+
|
|
112
|
+
| Storey | What lives there | Where |
|
|
113
|
+
| ------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
114
|
+
| transport | one model, four answers | `ModelBackend` — three inside the package, two written outside it |
|
|
115
|
+
| session | one conversation, the guarantees | **this package**, and nothing above it |
|
|
116
|
+
| orchestration | several models, a policy, a tool loop | [`external/orchestrator`](external/orchestrator), [`external/agent`](external/agent) |
|
|
117
|
+
|
|
118
|
+
That is the direction this repo is developed in. The package does not grow up
|
|
119
|
+
the stairs; what grows is what stands on it, and [`external/`](external/) is
|
|
120
|
+
where that is tried — each package written the way a stranger would write it,
|
|
121
|
+
against `modelpact` at `file:../..`, through the `exports` map, with no path into
|
|
122
|
+
`src/`. Whatever the published API is not enough for shows up there first.
|
|
123
|
+
|
|
124
|
+
**A transport from outside.** [`external/webgpu-provider`](external/webgpu-provider)
|
|
125
|
+
is a model in the tab on WebGPU, through `@mlc-ai/web-llm`. It answers the same
|
|
126
|
+
four questions, and `describeContract` runs against it: 34 green. It also found
|
|
127
|
+
two real bugs — a published type that named a global consumers do not have, and a
|
|
128
|
+
backend that errored its own stream and landed in `unknown` instead of `aborted`.
|
|
129
|
+
Both fixed; both now have a guard. The demo depends on this package the way it
|
|
130
|
+
would on anything from npm.
|
|
131
|
+
|
|
132
|
+
**One storey up.** [`external/orchestrator`](external/orchestrator) puts Claude
|
|
133
|
+
from `claude -p` and a local Ollama model in one conversation, with a policy
|
|
134
|
+
deciding which side answers. Its first version was a `ModelBackend` composed of
|
|
135
|
+
two — and it passed the suite while every guarantee leaked: a meter over two
|
|
136
|
+
windows, an overflow that meant nothing for the other side. Rebuilt one storey
|
|
137
|
+
up it needed **nothing new from the package**: `open({ history })` was already
|
|
138
|
+
the door for handing a side the turns it missed. That is the test of whether a
|
|
139
|
+
storey is right.
|
|
140
|
+
|
|
141
|
+
**An agent, on the same storey.** [`external/agent`](external/agent) is a
|
|
142
|
+
tool-calling loop over a `Brain` — two methods, `ask` and `record` — which is an
|
|
143
|
+
`AiSession` or an orchestrator behind a two-line adapter. This contract has no
|
|
144
|
+
tool protocol, and it did not need one: a tool call is a schema, honoured or
|
|
145
|
+
refused and never ignored, and where a backend refuses (`claude -p` does,
|
|
146
|
+
measured) the refusal picks prose mode rather than ending the run. A tool result
|
|
147
|
+
goes back as the next user turn. The reference agent's shape survived; its
|
|
148
|
+
`bash` tool and the hundred lines of path containment it needs did not.
|
|
149
|
+
|
|
150
|
+
Three packages, three storeys, and the thing they share is fifteen exports and
|
|
151
|
+
a test suite. Each export is there because something outside needed it; what is
|
|
152
|
+
not there — storage, a router, a tool protocol, a third role — is not missing,
|
|
153
|
+
it lives upstairs. The next transport is an afternoon and a green suite. The
|
|
154
|
+
next policy or loop is a consumer, not a feature.
|
|
155
|
+
|
|
156
|
+
## Quick start
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
npm install modelpact
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { makeMockProvider } from "modelpact";
|
|
164
|
+
|
|
165
|
+
const access = await makeMockProvider().access();
|
|
166
|
+
if (access.kind !== "ready") return; // unavailable, or needs a download first
|
|
167
|
+
|
|
168
|
+
const opened = await access.open({ system: "Answer in one sentence." });
|
|
169
|
+
if (!opened.ok) return;
|
|
170
|
+
|
|
171
|
+
const reply = await opened.value.prompt("What is this page about?");
|
|
172
|
+
console.log(reply.ok ? reply.value : reply.error.kind);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Swap `makeMockProvider` for any other provider and nothing below it changes.
|
|
176
|
+
That is the whole point of the line.
|
|
177
|
+
|
|
178
|
+
> **What is in.** The contract, its type-level test, the lifecycle, the
|
|
179
|
+
> conformance suite, and three backends — the mock, Ollama, and Chrome's
|
|
180
|
+
> built-in model. Everything a backend needs is exported, and two more backends
|
|
181
|
+
> plus an orchestrator and an agent have been written outside the package on
|
|
182
|
+
> exactly that — see [The step, and what stands on it](#the-step-and-what-stands-on-it).
|
|
183
|
+
|
|
184
|
+
### Bring your own backend
|
|
185
|
+
|
|
186
|
+
A backend answers four questions. The lifecycle does the rest — one generation
|
|
187
|
+
at a time, an abort that leaves the session open, an overflow that fires once, a
|
|
188
|
+
close that refuses everything after it.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { createProvider, ok, type ModelBackend } from "modelpact";
|
|
192
|
+
|
|
193
|
+
const backend: ModelBackend = {
|
|
194
|
+
name: "echo",
|
|
195
|
+
modalities: ["text"],
|
|
196
|
+
availability: () => ({ kind: "ready" }),
|
|
197
|
+
connect: () => Promise.resolve(ok(model)), // your `generateStream`, `usage`, `dispose`
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const echo = createProvider(backend);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Then prove it behaves like the others:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { describeContract } from "modelpact/testing";
|
|
207
|
+
|
|
208
|
+
describeContract("echo", () => echo);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`modelpact/testing` needs `vitest`, which is an optional peer dependency: an app
|
|
212
|
+
that only consumes a provider never loads it.
|
|
213
|
+
|
|
214
|
+
### Several backends in one app
|
|
215
|
+
|
|
216
|
+
The list of names belongs to the app, not to this package — a backend written
|
|
217
|
+
elsewhere names itself. Your registry is where the set is known, so a switch over
|
|
218
|
+
it stays exhaustive and a string out of storage cannot pretend to be a name.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { defineProviders, findProviderName } from "modelpact";
|
|
222
|
+
|
|
223
|
+
const PROVIDERS = defineProviders({ echo, mock: makeMockProvider() });
|
|
224
|
+
|
|
225
|
+
const name = findProviderName(
|
|
226
|
+
PROVIDERS,
|
|
227
|
+
localStorage.getItem("provider") ?? "",
|
|
228
|
+
);
|
|
229
|
+
const provider = name === null ? null : PROVIDERS[name];
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Status
|
|
233
|
+
|
|
234
|
+
**Versioned, and released by machine.** Semantic versioning from conventional
|
|
235
|
+
commits: [release-please](https://github.com/googleapis/release-please) opens
|
|
236
|
+
the release PR, [`CHANGELOG.md`](CHANGELOG.md) is written from the history, and
|
|
237
|
+
the tagged tree is what `npm publish` builds, with provenance. A `!` in a commit
|
|
238
|
+
is a major, and 2.0 was one — four public names changed for the better, and the
|
|
239
|
+
changelog says which.
|
|
240
|
+
|
|
241
|
+
**Small on purpose.** ESM only, zero runtime dependencies, and the whole entry
|
|
242
|
+
is 13.6 kB minified, 4.9 kB gzipped, before tree-shaking takes the providers
|
|
243
|
+
you do not import. `modelpact/testing` is a second entry with `vitest` as an
|
|
244
|
+
optional peer, so an app that only consumes a provider never loads it.
|
|
245
|
+
|
|
246
|
+
**Where it runs.** Node ≥ 22 for the Ollama backend and the suites; any current
|
|
247
|
+
browser for a session over `fetch`; Chrome for the built-in model. The
|
|
248
|
+
published declarations are checked by three outside packages with `skipLibCheck`
|
|
249
|
+
off and `types: []` — if a `.d.ts` ever names a global you do not have, that
|
|
250
|
+
build goes red before yours does.
|
|
251
|
+
|
|
252
|
+
**What a pull request has to pass.** Typecheck, lint, format, the vitest suites,
|
|
253
|
+
nine Playwright specs in Chromium, and the three packages under `external/` —
|
|
254
|
+
their own tests and their surface guards — on every change, in one run.
|
|
255
|
+
|
|
256
|
+
**Next.** An OpenAI-compatible HTTP backend: one transport for
|
|
257
|
+
`/v1/chat/completions`, which is the cloud APIs, vLLM, LM Studio and a
|
|
258
|
+
llama.cpp server at once. Same four answers, same suite.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## A Deep Dive for Techno-Geeks
|
|
263
|
+
|
|
264
|
+
`@types/dom-chromium-ai` follows the IDL, and the IDL is looser than
|
|
265
|
+
[the spec](docs/mdn/prompt_api/spec.md):
|
|
266
|
+
several states the algorithm rejects at runtime are writable in the types. A TS
|
|
267
|
+
error at the keyboard beats a `TypeError` in the browser, so
|
|
268
|
+
[`patches/@types+dom-chromium-ai+0.0.17.patch`](patches/@types+dom-chromium-ai+0.0.17.patch) closes the gap.
|
|
269
|
+
|
|
270
|
+
The patch is applied by `patch-package` on `prepare`, which runs for this repo
|
|
271
|
+
and never for anyone installing the published package. `skipLibCheck` is
|
|
272
|
+
deliberately **off** in `tsconfig.json`: these declarations are the only
|
|
273
|
+
third-party ones in the project, and type-checking them is how a patch that
|
|
274
|
+
stops applying cleanly gets caught.
|
|
275
|
+
|
|
276
|
+
## The contract
|
|
277
|
+
|
|
278
|
+
[`src/types`](src/types) is seven files and no runtime dependencies. Each holds one
|
|
279
|
+
layer, and a fact lives on the type it is about: what a field means sits on the
|
|
280
|
+
type rather than at every place it is read, so go-to-definition walks the
|
|
281
|
+
reasoning instead of finding it restated.
|
|
282
|
+
|
|
283
|
+
| File | Holds |
|
|
284
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
285
|
+
| [`foundations.ts`](src/types/foundations.ts) | `Result`, and the branded `Tokens`, `Fraction`, `JsonSchema`. |
|
|
286
|
+
| [`messages.ts`](src/types/messages.ts) | `AiMessage`, `Modality`, `ModelRequest`. |
|
|
287
|
+
| [`usage.ts`](src/types/usage.ts) | `ContextUsage` — unknown, unbounded or bounded — and `UsageKind`. |
|
|
288
|
+
| [`failures.ts`](src/types/failures.ts) | `AiFailure` and `FailureKind`, the mapping `failureFromError`, `AiError`. |
|
|
289
|
+
| [`session.ts`](src/types/session.ts) | `AiSession`, `ModelAccess` and `AccessKind`, `DownloadMonitor`, the option types. |
|
|
290
|
+
| [`provider.ts`](src/types/provider.ts) | `ProviderName`, `AiProvider`. |
|
|
291
|
+
| [`backend.ts`](src/types/backend.ts) | `ModelBackend`, `ModelConnection` — the four answers a provider supplies. |
|
|
292
|
+
|
|
293
|
+
Four ideas carry the rest:
|
|
294
|
+
|
|
295
|
+
**Refusals are values.** `Result<T, AiFailure>` on the adapter boundary, so the
|
|
296
|
+
failure path is in the signature and cannot be skipped in silence. Inside a
|
|
297
|
+
provider's own implementation, exceptions are fine: `Result` spreads into every
|
|
298
|
+
signature it touches, and only the boundary is worth that cost.
|
|
299
|
+
|
|
300
|
+
**The failure vocabulary is cut by the caller's next move.** Not by exception
|
|
301
|
+
name. Kinds merge where the reaction would be the same and split where it
|
|
302
|
+
differs, even when the spec throws one exception for both — `unsupported-config`
|
|
303
|
+
(wrong environment) and `unsupported-input` (wrong message) are one
|
|
304
|
+
`NotSupportedError` upstream and two kinds here, because one is fixed by asking
|
|
305
|
+
for less and the other by sending something else.
|
|
306
|
+
|
|
307
|
+
**Mistakes are made unwritable, not guarded against.** `ModelAccess` carries
|
|
308
|
+
`open` only on the variants where opening can work, so "create a session on an
|
|
309
|
+
unavailable model" is not an error to check for at runtime — there is no
|
|
310
|
+
expression for it.
|
|
311
|
+
|
|
312
|
+
**A tag is a string, not an enum.** Every union here is discriminated by a
|
|
313
|
+
`kind`, and `AccessKind`, `FailureKind` and `UsageKind` name those sets for a
|
|
314
|
+
`Record` or a signature. They are derived aliases, so a new variant is in them
|
|
315
|
+
already, and they stay types: `"ready"` still passes where one is asked for, and
|
|
316
|
+
nothing of them reaches your bundle. An enum member would be a value, and a
|
|
317
|
+
nominal one — your own `"ready"` would stop being assignable to ours.
|
|
318
|
+
|
|
319
|
+
**A plain number is not a measurement.** `Tokens` and `Fraction` are separate
|
|
320
|
+
brands over `number`, because a ratio and a percentage (0.5 against 50) are both
|
|
321
|
+
numbers and swapping them is silent. A constructor that validates is the only way
|
|
322
|
+
in.
|
|
323
|
+
|
|
324
|
+
Four compiler flags beyond `strict` are load-bearing, `exactOptionalPropertyTypes`
|
|
325
|
+
most of all: without it `{ system: undefined }` passes as `SessionOptions`, and
|
|
326
|
+
every invariant that rests on an absent optional field falls apart.
|
|
327
|
+
|
|
328
|
+
### The types have their own test
|
|
329
|
+
|
|
330
|
+
[`src/types.test-d.ts`](src/types.test-d.ts) is compiled, never run. Every line
|
|
331
|
+
that must **not** compile carries a `@ts-expect-error`, and TypeScript reports
|
|
332
|
+
`TS2578: Unused '@ts-expect-error' directive` when the expected error fails to
|
|
333
|
+
appear. So the file builds exactly while each listed state stays
|
|
334
|
+
unrepresentable — loosen a type and the build breaks.
|
|
335
|
+
|
|
336
|
+
It covers twelve of them:
|
|
337
|
+
|
|
338
|
+
1. A session cannot be opened on an unavailable model
|
|
339
|
+
2. A result cannot be used without handling the failure
|
|
340
|
+
3. Each failure carries only its own fields
|
|
341
|
+
4. An unbounded window cannot be subtracted from by accident
|
|
342
|
+
5. A system turn cannot be smuggled into the history
|
|
343
|
+
6. A schema is not just any object
|
|
344
|
+
7. Availability is asked for a specific request
|
|
345
|
+
8. The provider list is the app's, and switches over it stay exhaustive
|
|
346
|
+
9. The stream stays a stream
|
|
347
|
+
10. The monitor is the platform's, and ours passes for it
|
|
348
|
+
11. A kind alias names the set without closing it to literals
|
|
349
|
+
12. The record is a snapshot, not a handle
|
|
350
|
+
|
|
351
|
+
It falls under the project tsconfig's `include`, so `npm run typecheck` checks
|
|
352
|
+
it. Vitest deliberately misses it — `include` there is `*.test.ts`.
|
|
353
|
+
|
|
354
|
+
## Contributing
|
|
355
|
+
|
|
356
|
+
Conventional commits — release-please reads them — and a pull request against
|
|
357
|
+
`main`; CI is the reviewer that has to say yes first. Everything it runs, runs
|
|
358
|
+
locally:
|
|
359
|
+
|
|
360
|
+
```sh
|
|
361
|
+
npm ci
|
|
362
|
+
npm run typecheck && npm run lint && npm run format:check && npm test
|
|
363
|
+
npm run test:e2e # Chromium; the demo server starts itself
|
|
364
|
+
npm run external && npm run external:orchestrator && npm run external:agent
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
A backend is the most useful thing to bring. Four answers, `createProvider`,
|
|
368
|
+
and `describeContract` green — see [Bring your own backend](#bring-your-own-backend).
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abort mechanics every provider shares: one session-wide signal mixed
|
|
3
|
+
* into every generation, so a close ends the in-flight calls and the later
|
|
4
|
+
* ones alike.
|
|
5
|
+
*/
|
|
6
|
+
import type { AiFailure } from "../types/failures.js";
|
|
7
|
+
export declare function abortFailure(signal: AbortSignal): AiFailure;
|
|
8
|
+
/**
|
|
9
|
+
* Skips `AbortSignal.any` when there is nothing to link: `any([s])` allocates
|
|
10
|
+
* a fresh signal and a listener on the original, for nothing. The `reason`
|
|
11
|
+
* survives either way — it propagates by identity, not by copy.
|
|
12
|
+
*/
|
|
13
|
+
export declare const linkSignals: (sessionSignal: AbortSignal, callerSignal?: AbortSignal) => AbortSignal;
|
|
14
|
+
//# sourceMappingURL=abort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../../src/helpers/abort.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAO3D;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,GACtB,eAAe,WAAW,EAC1B,eAAe,WAAW,KACzB,WAGiD,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The abort mechanics every provider shares: one session-wide signal mixed
|
|
3
|
+
* into every generation, so a close ends the in-flight calls and the later
|
|
4
|
+
* ones alike.
|
|
5
|
+
*/
|
|
6
|
+
export function abortFailure(signal) {
|
|
7
|
+
const reason = signal.reason;
|
|
8
|
+
return {
|
|
9
|
+
kind: "aborted",
|
|
10
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
11
|
+
cause: reason,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Skips `AbortSignal.any` when there is nothing to link: `any([s])` allocates
|
|
16
|
+
* a fresh signal and a listener on the original, for nothing. The `reason`
|
|
17
|
+
* survives either way — it propagates by identity, not by copy.
|
|
18
|
+
*/
|
|
19
|
+
export const linkSignals = (sessionSignal, callerSignal) => callerSignal === undefined
|
|
20
|
+
? sessionSignal
|
|
21
|
+
: AbortSignal.any([sessionSignal, callerSignal]);
|
|
22
|
+
//# sourceMappingURL=abort.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abort.js","sourceRoot":"","sources":["../../src/helpers/abort.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,UAAU,YAAY,CAAC,MAAmB;IAC9C,MAAM,MAAM,GAAY,MAAM,CAAC,MAAM,CAAC;IACtC,OAAO;QACL,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;QACjE,KAAK,EAAE,MAAM;KACd,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,aAA0B,EAC1B,YAA0B,EACb,EAAE,CACf,YAAY,KAAK,SAAS;IACxB,CAAC,CAAC,aAAa;IACf,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session is idle, generating, or closed: one cell, not three flags.
|
|
3
|
+
*
|
|
4
|
+
* A `running` name beside an `aborted` signal beside a `disposed` boolean lets
|
|
5
|
+
* every combination compile — closed but busy, a turn with no signal to end
|
|
6
|
+
* it, an `end()` from a turn abandoned two turns ago freeing the one running
|
|
7
|
+
* now. Here the phase is a union, the running phase carries the signal that
|
|
8
|
+
* ends it, and it is its own token: `end` compares identity, so a stale hook
|
|
9
|
+
* has nothing to free.
|
|
10
|
+
*
|
|
11
|
+
* Shared rather than written per provider, for the reason `overflow.ts` is
|
|
12
|
+
* shared: the contract suite would see each one refuse and never notice they
|
|
13
|
+
* refuse different things.
|
|
14
|
+
*/
|
|
15
|
+
import { type Result } from "../types/foundations.js";
|
|
16
|
+
import type { AiFailure } from "../types/failures.js";
|
|
17
|
+
/** The two contract calls that generate; `usage` and `close` never conflict. */
|
|
18
|
+
export type GeneratingCall = "prompt" | "promptStream";
|
|
19
|
+
/** Proof that the door was open: the only way to hold one is `begin` agreeing. */
|
|
20
|
+
export interface RunningTurn {
|
|
21
|
+
readonly callName: GeneratingCall;
|
|
22
|
+
/** The session's own abort linked with the caller's; hand this to the backend. */
|
|
23
|
+
readonly signal: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
export interface SessionLifetime {
|
|
26
|
+
/** A running turn, or what this call owes the caller: `busy` while another runs, `aborted` once closed. */
|
|
27
|
+
readonly begin: (callName: GeneratingCall, signal?: AbortSignal) => Result<RunningTurn, AiFailure>;
|
|
28
|
+
/** Ignored unless `turn` is the one running, which is what keeps an abandoned turn from freeing its successor. */
|
|
29
|
+
readonly end: (turn: RunningTurn) => void;
|
|
30
|
+
/** `closed-now` once per session; the caller releases the model on that one. */
|
|
31
|
+
readonly close: () => "closed-now" | "already-closed";
|
|
32
|
+
}
|
|
33
|
+
export declare function createSessionLifetime(): SessionLifetime;
|
|
34
|
+
//# sourceMappingURL=lifetime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifetime.d.ts","sourceRoot":"","sources":["../../src/helpers/lifetime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGtD,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,cAAc,CAAC;AAEvD,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAYD,MAAM,WAAW,eAAe;IAC9B,2GAA2G;IAC3G,QAAQ,CAAC,KAAK,EAAE,CACd,QAAQ,EAAE,cAAc,EACxB,MAAM,CAAC,EAAE,WAAW,KACjB,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACpC,kHAAkH;IAClH,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,CAAC;IAC1C,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,YAAY,GAAG,gBAAgB,CAAC;CACvD;AAaD,wBAAgB,qBAAqB,IAAI,eAAe,CA2CvD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session is idle, generating, or closed: one cell, not three flags.
|
|
3
|
+
*
|
|
4
|
+
* A `running` name beside an `aborted` signal beside a `disposed` boolean lets
|
|
5
|
+
* every combination compile — closed but busy, a turn with no signal to end
|
|
6
|
+
* it, an `end()` from a turn abandoned two turns ago freeing the one running
|
|
7
|
+
* now. Here the phase is a union, the running phase carries the signal that
|
|
8
|
+
* ends it, and it is its own token: `end` compares identity, so a stale hook
|
|
9
|
+
* has nothing to free.
|
|
10
|
+
*
|
|
11
|
+
* Shared rather than written per provider, for the reason `overflow.ts` is
|
|
12
|
+
* shared: the contract suite would see each one refuse and never notice they
|
|
13
|
+
* refuse different things.
|
|
14
|
+
*/
|
|
15
|
+
import { err, ok } from "../types/foundations.js";
|
|
16
|
+
import { abortFailure, linkSignals } from "./abort.js";
|
|
17
|
+
/** No signal to read a reason from: closing is not an abort with a cause, it is the end of the session. */
|
|
18
|
+
const CLOSED_FAILURE = {
|
|
19
|
+
kind: "aborted",
|
|
20
|
+
reason: "the session is closed",
|
|
21
|
+
};
|
|
22
|
+
const busyFailure = (callName) => ({
|
|
23
|
+
kind: "busy",
|
|
24
|
+
detail: `${callName} is already running on this session`,
|
|
25
|
+
});
|
|
26
|
+
export function createSessionLifetime() {
|
|
27
|
+
let phase = { kind: "idle" };
|
|
28
|
+
return {
|
|
29
|
+
begin: (callName, signal) => {
|
|
30
|
+
switch (phase.kind) {
|
|
31
|
+
case "closed":
|
|
32
|
+
return err(CLOSED_FAILURE);
|
|
33
|
+
case "generating":
|
|
34
|
+
return err(busyFailure(phase.turn.callName));
|
|
35
|
+
case "idle": {
|
|
36
|
+
// The caller's own signal, not the linked one: its reason is the one
|
|
37
|
+
// worth reporting, and linking an aborted signal only to read it back
|
|
38
|
+
// is work for nothing.
|
|
39
|
+
if (signal?.aborted === true)
|
|
40
|
+
return err(abortFailure(signal));
|
|
41
|
+
const abortController = new AbortController();
|
|
42
|
+
const turn = {
|
|
43
|
+
callName,
|
|
44
|
+
signal: linkSignals(abortController.signal, signal),
|
|
45
|
+
};
|
|
46
|
+
phase = { kind: "generating", turn, abortController };
|
|
47
|
+
return ok(turn);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
end: (turn) => {
|
|
52
|
+
if (phase.kind !== "generating")
|
|
53
|
+
return;
|
|
54
|
+
if (phase.turn !== turn)
|
|
55
|
+
return;
|
|
56
|
+
phase = { kind: "idle" };
|
|
57
|
+
},
|
|
58
|
+
close: () => {
|
|
59
|
+
if (phase.kind === "closed")
|
|
60
|
+
return "already-closed";
|
|
61
|
+
// Moved before the abort: the listeners run synchronously, and one of
|
|
62
|
+
// them reaching back in must find a closed session rather than this one.
|
|
63
|
+
const previousPhase = phase;
|
|
64
|
+
phase = { kind: "closed" };
|
|
65
|
+
if (previousPhase.kind === "generating")
|
|
66
|
+
previousPhase.abortController.abort();
|
|
67
|
+
return "closed-now";
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=lifetime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifetime.js","sourceRoot":"","sources":["../../src/helpers/lifetime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAkCvD,2GAA2G;AAC3G,MAAM,cAAc,GAAc;IAChC,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,uBAAuB;CAChC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAa,EAAE,CAAC,CAAC;IAC5D,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,GAAG,QAAQ,qCAAqC;CACzD,CAAC,CAAC;AAEH,MAAM,UAAU,qBAAqB;IACnC,IAAI,KAAK,GAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAEpC,OAAO;QACL,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE;YAC1B,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnB,KAAK,QAAQ;oBACX,OAAO,GAAG,CAAC,cAAc,CAAC,CAAC;gBAC7B,KAAK,YAAY;oBACf,OAAO,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC/C,KAAK,MAAM,CAAC,CAAC,CAAC;oBACZ,qEAAqE;oBACrE,sEAAsE;oBACtE,uBAAuB;oBACvB,IAAI,MAAM,EAAE,OAAO,KAAK,IAAI;wBAAE,OAAO,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;oBAC/D,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;oBAC9C,MAAM,IAAI,GAAG;wBACX,QAAQ;wBACR,MAAM,EAAE,WAAW,CAAC,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC;qBACpD,CAAC;oBACF,KAAK,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;oBACtD,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;gBAClB,CAAC;YACH,CAAC;QACH,CAAC;QAED,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;YACZ,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;gBAAE,OAAO;YACxC,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;gBAAE,OAAO;YAChC,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC3B,CAAC;QAED,KAAK,EAAE,GAAG,EAAE;YACV,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO,gBAAgB,CAAC;YACrD,sEAAsE;YACtE,yEAAyE;YACzE,MAAM,aAAa,GAAG,KAAK,CAAC;YAC5B,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;YAC3B,IAAI,aAAa,CAAC,IAAI,KAAK,YAAY;gBACrC,aAAa,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YACxC,OAAO,YAAY,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download progress as the platform reports it: an `EventTarget` that fires
|
|
3
|
+
* `downloadprogress`.
|
|
4
|
+
*
|
|
5
|
+
* The Prompt API hands `create()` a `CreateMonitor` and fires a `ProgressEvent`
|
|
6
|
+
* on it. This is the same object for a provider with no browser behind it, and
|
|
7
|
+
* both satisfy `DownloadMonitor` from `../types/session.ts` —
|
|
8
|
+
* `src/types.test-d.ts` checks the ambient declaration against it.
|
|
9
|
+
*
|
|
10
|
+
* `ProgressEvent` is not a global in Node 22 — `typeof ProgressEvent` is
|
|
11
|
+
* `"undefined"` there, while `Event`, `EventTarget` and `CustomEvent` are
|
|
12
|
+
* present — and the vitest suites run in the node environment. So the event is
|
|
13
|
+
* a subclass of `Event` carrying the three `ProgressEvent` fields, which is
|
|
14
|
+
* what a listener reads. `CustomEvent` was the alternative and puts the numbers
|
|
15
|
+
* behind `detail`, where a listener written against the spec would not look.
|
|
16
|
+
*/
|
|
17
|
+
import type { Fraction } from "../types/foundations.js";
|
|
18
|
+
import type { DownloadMonitor } from "../types/session.js";
|
|
19
|
+
export declare const DOWNLOAD_PROGRESS = "downloadprogress";
|
|
20
|
+
/**
|
|
21
|
+
* `total` is 1 and `loaded` is the ratio. MDN names the normalization on
|
|
22
|
+
* `ProgressEvent` itself ("if using 1 as a total, then loaded would be a
|
|
23
|
+
* decimal value between 0 and 1"), and it settles the one thing the Prompt API
|
|
24
|
+
* docs disagree about: `e.loaded / e.total` and a bare `e.loaded` are the same
|
|
25
|
+
* number here.
|
|
26
|
+
*/
|
|
27
|
+
export declare class DownloadProgressEvent extends Event {
|
|
28
|
+
readonly loaded: Fraction;
|
|
29
|
+
readonly lengthComputable = true;
|
|
30
|
+
readonly total = 1;
|
|
31
|
+
constructor(loaded: Fraction);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Our `CreateMonitor`. `report` is not part of `DownloadMonitor`, so a caller
|
|
35
|
+
* holding the contract type can listen but cannot invent progress —
|
|
36
|
+
* `src/types.test-d.ts` checks that it cannot.
|
|
37
|
+
*/
|
|
38
|
+
export declare class ProgressMonitor extends EventTarget implements DownloadMonitor {
|
|
39
|
+
#private;
|
|
40
|
+
/**
|
|
41
|
+
* An `onevent` property is not free with `EventTarget`: only interfaces the
|
|
42
|
+
* platform defines have one, and it is a listener registration behind an
|
|
43
|
+
* accessor — assigning twice replaces, assigning null unsubscribes. Here it
|
|
44
|
+
* also carries the event's type, which `addEventListener` cannot: see the
|
|
45
|
+
* note on `DownloadMonitor`.
|
|
46
|
+
*/
|
|
47
|
+
get ondownloadprogress(): ((event: ProgressEvent) => void) | null;
|
|
48
|
+
set ondownloadprogress(handler: ((event: ProgressEvent) => void) | null);
|
|
49
|
+
/** Dropped rather than dispatched when it does not move forward — the contract's non-decreasing guarantee. */
|
|
50
|
+
report(loaded: Fraction): void;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../../src/helpers/monitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,eAAO,MAAM,iBAAiB,qBAAqB,CAAC;AAEpD;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAIlC,QAAQ,CAAC,MAAM,EAAE,QAAQ;IAHrC,QAAQ,CAAC,gBAAgB,QAAQ;IACjC,QAAQ,CAAC,KAAK,KAAK;gBAEE,MAAM,EAAE,QAAQ;CAGtC;AAED;;;;GAIG;AACH,qBAAa,eAAgB,SAAQ,WAAY,YAAW,eAAe;;IAIzE;;;;;;OAMG;IACH,IAAI,kBAAkB,IAAI,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAEhE;IAED,IAAI,kBAAkB,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,EAWtE;IAED,8GAA8G;IAC9G,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI;CAK/B"}
|