acrylblends 0.7.7 → 0.7.8
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/dist/index.d.ts +1 -1
- package/dist/index.js +59 -19
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +1 -1
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/agents/index.md +18 -3
- package/docs/framework/getting-started/desktop.md +2 -2
- package/docs/framework/guides/client-modules.md +4 -3
- package/docs/framework/guides/data.md +6 -6
- package/docs/framework/helpers/streaming.md +12 -27
- package/docs/framework/upstream.json +2 -2
- package/lib/browser/read-sse.js +84 -0
- package/lib/db/sqlite.d.ts +1 -1
- package/lib/db.d.ts +1 -84
- package/lib/index-BLTYsyc1.d.ts +93 -0
- package/lib/index-D4FAsBZu.d.ts +85 -0
- package/lib/kv/db.d.ts +2 -2
- package/lib/kv/json.d.ts +1 -1
- package/lib/kv/json.js +87 -24
- package/lib/kv.d.ts +1 -92
- package/lib/node-server.d.ts +3 -3
- package/lib/node-server.js +13 -8
- package/lib/read-sse.d.ts +33 -0
- package/lib/read-sse.js +84 -0
- package/package.json +5 -1
- package/plugins/acryl-db/plugin.json +1 -1
- package/plugins/acryl-db/upstream.json +2 -2
- package/plugins/acryl-db-sqlite/upstream.json +1 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-kv/plugin.json +1 -1
- package/plugins/acryl-kv/upstream.json +1 -1
- package/plugins/acryl-kv-db/upstream.json +1 -1
- package/plugins/acryl-kv-json/src/open.ts +86 -22
- package/plugins/acryl-kv-json/upstream.json +2 -2
- package/plugins/acryl-node-server/src/client-modules.ts +10 -9
- package/plugins/acryl-node-server/upstream.json +2 -2
- package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
- package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/builder/index.ts +2 -2
- package/plugins/builder/plugin.json +1 -1
- package/plugins/desktop/main.ts +3 -3
- package/plugins/desktop/plugin.json +2 -2
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +2 -2
- package/plugins/desktop-web/plugin.json +2 -2
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/extensions/index.ts +19 -1
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +1 -1
- package/plugins/manifest/lock.ts +7 -3
- package/plugins/manifest/upgrade-cli.ts +1 -1
- package/plugins/save/cli.ts +1 -1
- package/plugins/save/plugin.json +1 -1
- package/shared/AGENTS.core.md +10 -10
- package/shared/DEFAULTS.md +40 -0
- package/shared/SKILL.md +14 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/README.md +6 -6
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +8 -8
- package/templates/agent/src/app.ts +28 -15
- package/templates/agent/src/main.ts +2 -2
- package/templates/agent/src/routes.ts +5 -5
- package/templates/agent/test/agent.test.ts +1 -1
- package/templates/agent/test/app.test.ts +1 -1
- package/templates/agent/test/builder.test.ts +1 -1
- package/templates/agent/test/model.test.ts +1 -1
- package/templates/agent/tsconfig.json +48 -50
- package/templates/blank/AGENTS.md +1 -1
- package/templates/blank/README.md +2 -2
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +8 -8
- package/templates/blank/src/about.tsx +2 -2
- package/templates/blank/src/app.ts +4 -4
- package/templates/blank/src/main.ts +1 -1
- package/templates/blank/src/routes.ts +1 -1
- package/templates/blank/test/app.test.ts +1 -1
- package/templates/blank/tsconfig.json +1 -1
- package/templates/todo/README.md +1 -1
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +8 -8
- package/templates/todo/src/app.ts +5 -5
- package/templates/todo/src/main.ts +1 -1
- package/templates/todo/src/todo.ts +4 -4
- package/templates/todo/test/todo.test.ts +1 -1
- package/templates/todo/tsconfig.json +1 -1
- package/templates/todo/upstream.json +2 -2
- package/lib/database-D_UN6We8.d.ts +0 -44
- package/lib/kv-DWDm0Lja.d.ts +0 -39
|
@@ -5,14 +5,29 @@ A coding agent (Claude Code, Codex, OpenCode, Copilot and others) builds on Acry
|
|
|
5
5
|
|
|
6
6
|
- `docs/framework/`: these pages. Start at `README.md`; `api/summary.md` is the one-page summary of the API.
|
|
7
7
|
- `AGENTS.md`: where the API is, how a feature is built (an `Acryl` app mounted with `ctx.router.route`, a plugin registered in `src/app.ts` and `blend.yaml`, a state cell),
|
|
8
|
-
the things that surprise people, and what must pass before a task is done (`npm test` and `npm run check`).
|
|
9
|
-
- `CLAUDE.md`:
|
|
8
|
+
the defaults and how each is swapped, the things that surprise people, and what must pass before a task is done (`npm test` and `npm run check`).
|
|
9
|
+
- `CLAUDE.md`: the same text as `AGENTS.md`, so tools that read either file read the same guide.
|
|
10
|
+
- One skill, `app-guide`, in `.claude/skills/app-guide/SKILL.md` and `.agents/skills/app-guide/SKILL.md` (the same text): where to read first, the one plugin folder, the defaults, the door, and what "done" means.
|
|
11
|
+
- `.gitignore` lists `.acryl/`, the folder the door keeps its secret and state in, so `git add -A` stages none of it.
|
|
12
|
+
|
|
13
|
+
## One plugin folder
|
|
14
|
+
|
|
15
|
+
Every plugin of an app is a folder in `plugins/<name>/`, at the root of the app: the library plugins `acrylblends add <name>` copies (listed in `plugins/library.json`), and the
|
|
16
|
+
plugins you or an agent write. There is no `src/plugins/` and no `extensions/`. An app made before this layout keeps working for one release: the CLI reads its `src/plugins/` and
|
|
17
|
+
`extensions/` and warns; move them with `git mv src/plugins plugins`, fix the paths in `tsconfig.json`, `package.json` and `src/`, and `git mv extensions/<name> plugins/<name>`.
|
|
18
|
+
|
|
19
|
+
## Defaults, and how to swap each
|
|
20
|
+
|
|
21
|
+
The framework is not rigid: every choice is a plugin behind a port, and the default is only a fallback. A new app names each default in `AGENTS.md`: records on the `db` port
|
|
22
|
+
with SQLite, small state on the `kv` port with one JSON file, the Node server, the browser as the window (`acrylblends add desktop` adds Electron), browser code through
|
|
23
|
+
`clientModules`. A swap changes the one file that opens or mounts the choice (the opener, `src/db.ts`; or `src/app.ts` in a Blend app), because nothing else imports a provider
|
|
24
|
+
(`acrylblends check-deps` checks it). An outside tool, such as a database server or a bundler, joins the app as a provider in `plugins/<name>/` that implements the port.
|
|
10
25
|
|
|
11
26
|
## What an agent can do inside a running app
|
|
12
27
|
|
|
13
28
|
The `acryl-app` template (a Cordis app) also lets an agent change the app while it runs:
|
|
14
29
|
|
|
15
|
-
1. The agent writes a plugin into `
|
|
30
|
+
1. The agent writes a plugin into `plugins/<name>/index.ts`. The plugin may import `acrylblends` and the other subpaths by name.
|
|
16
31
|
2. The app installs it as a child plugin, runs its `check(ctx)` with the plugin's own context, and keeps the previous version if the new one fails to start or to pass its check.
|
|
17
32
|
3. A plugin's routes are an `Acryl` app mounted with `ctx.router.route`, so removing the plugin removes its routes.
|
|
18
33
|
|
|
@@ -16,11 +16,11 @@ npm run desktop
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
In a Blend app, the same two commands (`npx acrylblends add desktop`, `npm install`) add the Blend form of the shell, and `npx acrylblends init my-app --template todo --desktop` creates a Blend app with it already in.
|
|
19
|
-
`acrylblends add desktop` chooses the right form: a Blend app (it has `
|
|
19
|
+
`acrylblends add desktop` chooses the right form: a Blend app (it has `plugins/instance`) or a plain Acryl app.
|
|
20
20
|
|
|
21
21
|
## What it adds
|
|
22
22
|
|
|
23
|
-
- `
|
|
23
|
+
- `plugins/desktop-web/` (a plain app) or `plugins/desktop/` (a Blend app): the window code. Only this folder imports `electron`; the app's own code never does.
|
|
24
24
|
- `electron` as a development dependency (pinned to 44.9.3) and the script `npm run desktop`.
|
|
25
25
|
- `.acrylblends/` in `.gitignore`: Electron's user data (window state, cache, logs) lives in `.acrylblends/electron` inside the app's own folder, never in the user's application-data folder.
|
|
26
26
|
|
|
@@ -149,8 +149,9 @@ For a request that streams its answer and a Stop button (an `AbortController` ke
|
|
|
149
149
|
- JSX in a client module always compiles for the DOM renderer (`acrylblends/jsx/dom`), whatever the server's `tsconfig.json` says. A file's
|
|
150
150
|
own `/** @jsxImportSource ... */` comment still wins.
|
|
151
151
|
- `acrylblends/jsx/dom`, its `jsx-runtime`, `jsx-dev-runtime` and `client` are one browser module (`@acrylblends/jsx-dom.js`), and
|
|
152
|
-
`acrylblends/client` is another (`@acrylblends/client.js`)
|
|
153
|
-
|
|
152
|
+
`acrylblends/client` is another (`@acrylblends/client.js`), and `acrylblends/read-sse` (the reader of a POST event stream, see
|
|
153
|
+
[Streaming](../helpers/streaming.md#reading-a-post-event-stream-in-the-browser)) a third (`@acrylblends/read-sse.js`). None imports anything. The same app
|
|
154
|
+
serves them, and each import is rewritten to a URL relative to the module, so the setup works under whatever path you route `clientModules` at.
|
|
154
155
|
- Import your own modules with their file names, as the browser fetches them: `import { label } from './parts/label.ts'`.
|
|
155
156
|
- Another package (`import confetti from 'canvas-confetti'`) is left as it is: add an
|
|
156
157
|
[import map](https://developer.mozilla.org/docs/Web/HTML/Element/script/type/importmap) to the page for it, or use a bundler.
|
|
@@ -185,7 +186,7 @@ Compile the client folder once when you build, as the `build` script of your `pa
|
|
|
185
186
|
}
|
|
186
187
|
```
|
|
187
188
|
|
|
188
|
-
It writes each module compiled to `dist/client` at the same path and file name, so the page's URLs stay the same, and copies the
|
|
189
|
+
It writes each module compiled to `dist/client` at the same path and file name, so the page's URLs stay the same, and copies the three
|
|
189
190
|
browser modules to `dist/client/@acrylblends/`. Run it where `typescript` is installed, before the development dependencies are removed.
|
|
190
191
|
|
|
191
192
|
With `NODE_ENV=production` and the `compiled` option set, `clientModules` serves only that folder and compiles nothing. A module that is in
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
An app that keeps records (users, notes, orders) keeps them in a SQL database, and reaches it through one small interface, `db`. Handlers and
|
|
4
4
|
stores take a `Db` as an argument and never import a driver, so the file or server behind it can change without touching them. SQLite is the
|
|
5
|
-
provider
|
|
5
|
+
provider in `acrylblends`. Postgres (or Supabase, through `DATABASE_URL`) is an optional plugin, `acryl-db-postgres`, that serves the same
|
|
6
|
+
interface; it is not part of `acrylblends` or the templates, and an app adds it only when it must use Postgres.
|
|
6
7
|
|
|
7
8
|
## Choosing
|
|
8
9
|
|
|
9
10
|
| The app | Use | State |
|
|
10
11
|
| --- | --- | --- |
|
|
11
12
|
| Normal web or desktop app, users, lists, orders | `db` with SQLite (the `notes` and `chat` templates) | available |
|
|
12
|
-
|
|
|
13
|
+
| Must use Postgres or Supabase | `db` with the optional `acryl-db-postgres` plugin | optional, not in `acrylblends` |
|
|
13
14
|
| Tiny, one person, a few screens | [`kv`](#a-tiny-app-kv), named JSON documents with no SQL | available |
|
|
14
|
-
| Agents or long tasks that must survive a restart | `durable`, in the same database as `db` | planned |
|
|
15
15
|
|
|
16
16
|
Start with `db` when the data has users, lists or relations: SQL is what people and coding agents already know. Start with `kv` when the whole
|
|
17
17
|
data is a handful of settings or a small list that one process reads and writes.
|
|
@@ -190,7 +190,7 @@ await db.transaction(async (tx) => {
|
|
|
190
190
|
|
|
191
191
|
## Writing SQL that moves to Postgres
|
|
192
192
|
|
|
193
|
-
The Postgres
|
|
193
|
+
The optional Postgres plugin serves the same interface. SQL that stays inside this list runs on both:
|
|
194
194
|
|
|
195
195
|
- Placeholders are `?` on every provider.
|
|
196
196
|
- Use `RETURNING` to get a new row, not a driver-specific "last id".
|
|
@@ -269,8 +269,8 @@ The rule is checked, not hoped for. The app names its opener once, in `package.j
|
|
|
269
269
|
}
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
-
`acrylblends check-deps` reads every `.ts`, `.tsx` and `.js` file in `src/`, `test/`, `tests/` and `
|
|
273
|
-
the opener imports a database driver, an opener such as `acrylblends/db/sqlite` or `acryl-db-sqlite`, or (outside `
|
|
272
|
+
`acrylblends check-deps` reads every `.ts`, `.tsx` and `.js` file in `src/`, `test/`, `tests/` and `plugins/`, and fails when a file other than
|
|
273
|
+
the opener imports a database driver, an opener such as `acrylblends/db/sqlite` or `acryl-db-sqlite`, or (outside `plugins/desktop`) `electron`.
|
|
274
274
|
It names the file and the line:
|
|
275
275
|
|
|
276
276
|
```
|
|
@@ -86,7 +86,7 @@ app.get('/sse', async (c) => {
|
|
|
86
86
|
A reader that closes the tab, loses the network or presses a Stop button should not leave your server working for nobody (a request to a paid model, a database scan). There are two signals, and they are not the same:
|
|
87
87
|
|
|
88
88
|
- **`c.req.raw.signal`** is the request's `AbortSignal`. It aborts when the client goes away. Pass it to anything that takes a `signal`, such as `fetch`, and the work stops with the client. This is the one to use for cancelling work.
|
|
89
|
-
- **`stream.onAbort(callback)`** and **`stream.aborted`** belong to the response stream. They report that the *reader of the response* was cancelled.
|
|
89
|
+
- **`stream.onAbort(callback)`** and **`stream.aborted`** belong to the response stream. They report that the *reader of the response* was cancelled, and they are set when the response body is cancelled, which is what the Node adapter does when the connection closes. Over a real connection, a browser that aborts its `fetch` (or closes the tab) fires both: the request signal and `stream.onAbort`. Under `app.request()` (in-process, no socket) they come apart, and this is how the upstream behaves too: aborting the `signal` you passed to `app.request()` fires `c.req.raw.signal` while `stream.aborted` stays `false`, and cancelling the reader of the response body fires `stream.onAbort` while `c.req.raw.signal` does not. A test that simulates the client leaving therefore aborts the request signal (for work cancelled through the signal) or cancels the body reader (for `stream.onAbort`), whichever the handler listens to. Use `c.req.raw.signal` for the work, and `stream.onAbort` for cleanup of the stream itself.
|
|
90
90
|
|
|
91
91
|
An abort event that has already fired does not fire again, so check `signal.aborted` before you wait for it:
|
|
92
92
|
|
|
@@ -160,31 +160,10 @@ app.post('/api/chat', async (c) => {
|
|
|
160
160
|
|
|
161
161
|
## Reading a POST event stream in the browser
|
|
162
162
|
|
|
163
|
-
`EventSource` can only send a GET request. For a POST (a chat message), read the response
|
|
163
|
+
`EventSource` can only send a GET request. For a POST (a chat message), call `fetch` and read the events with `readSSE` from `acrylblends/read-sse`. It is an async iterator over the events of a `text/event-stream` response, parsed by the rules of the SSE standard (LF, CRLF and CR line ends, several `data` lines joined with a line feed, comments, UTF-8 split across chunks):
|
|
164
164
|
|
|
165
165
|
```ts
|
|
166
|
-
|
|
167
|
-
const reader = body.getReader()
|
|
168
|
-
const decoder = new TextDecoder()
|
|
169
|
-
let buffer = ''
|
|
170
|
-
for (;;) {
|
|
171
|
-
const { done, value } = await reader.read()
|
|
172
|
-
if (done) return
|
|
173
|
-
// Line endings may be CRLF; a \r at the end of a chunk is completed by the next chunk, so normalise the whole buffer.
|
|
174
|
-
buffer = (buffer + decoder.decode(value, { stream: true })).replaceAll('\r\n', '\n')
|
|
175
|
-
const blocks = buffer.split('\n\n')
|
|
176
|
-
buffer = blocks.pop() ?? ''
|
|
177
|
-
for (const block of blocks) {
|
|
178
|
-
let event = 'message'
|
|
179
|
-
const data: string[] = []
|
|
180
|
-
for (const line of block.split('\n')) {
|
|
181
|
-
if (line.startsWith('event:')) event = line.slice(6).trim()
|
|
182
|
-
else if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /, '')) // the SSE spec strips one leading space only
|
|
183
|
-
}
|
|
184
|
-
on(event, data.join('\n'))
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
166
|
+
import { readSSE } from 'acrylblends/read-sse'
|
|
188
167
|
|
|
189
168
|
const controller = new AbortController() // controller.abort() is the Stop button
|
|
190
169
|
const response = await fetch('/api/chat', {
|
|
@@ -193,13 +172,19 @@ const response = await fetch('/api/chat', {
|
|
|
193
172
|
body: JSON.stringify({ message: 'Hello' }),
|
|
194
173
|
signal: controller.signal,
|
|
195
174
|
})
|
|
175
|
+
|
|
196
176
|
let answer = ''
|
|
197
|
-
await
|
|
177
|
+
for await (const { event, data } of readSSE(response)) {
|
|
198
178
|
if (event === 'token') answer += JSON.parse(data) as string
|
|
199
|
-
}
|
|
179
|
+
}
|
|
200
180
|
```
|
|
201
181
|
|
|
202
|
-
|
|
182
|
+
- Each event is `{ event, data, id?, retry? }`; `event` is `message` when the server named none, and `data` is a string (parse it yourself when the server sends JSON).
|
|
183
|
+
- Leaving the loop early (a `break`, a thrown error) cancels the response body, so the request to the server ends. Abort the `fetch` with its own `signal` for a Stop button; `readSSE(response, { signal })` also stops reading when a signal aborts.
|
|
184
|
+
- A response that is not `ok` throws with its status (the body is not parsed as events), and a response without a body throws.
|
|
185
|
+
- A client module served by `clientModules` can import it as it is (`import { readSSE } from 'acrylblends/read-sse'`): the import is pointed at a browser file the same app serves, like `acrylblends/client`. With a bundler it is an ordinary import.
|
|
186
|
+
|
|
187
|
+
A complete chat component is in the `chat` template of `create-acrylblends` (`npm create acrylblends@latest my-chat -- --template chat`).
|
|
203
188
|
|
|
204
189
|
## Error Handling
|
|
205
190
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "docs/framework",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "f132283c43337f5e1d58dfbbbc15786efb5e756c"
|
|
6
6
|
},
|
|
7
7
|
"files": 99,
|
|
8
|
-
"digest": "sha256:
|
|
8
|
+
"digest": "sha256:9b0fe3329319398ce1108a015a99247c001b02cfb30a953b4b180a73674e2aec"
|
|
9
9
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region ../../plugins/acryl-router/src/helpers/read-sse.ts
|
|
2
|
+
/**
|
|
3
|
+
* Read the events of a `text/event-stream` response as they arrive. Leaving the loop early (`break`, a thrown error) cancels the body, so the
|
|
4
|
+
* request to the server ends.
|
|
5
|
+
* @throws when the response is not `ok` (the status is in the message; the body is cancelled, not parsed), has no body, or fails while it is read.
|
|
6
|
+
*/
|
|
7
|
+
async function* readSSE(response, options = {}) {
|
|
8
|
+
if (!response.ok) {
|
|
9
|
+
await response.body?.cancel().catch(() => void 0);
|
|
10
|
+
throw new Error(`readSSE: the request failed with ${String(response.status)}${response.statusText === "" ? "" : ` ${response.statusText}`}`);
|
|
11
|
+
}
|
|
12
|
+
if (response.body === null) throw new Error("readSSE: the response has no body to read");
|
|
13
|
+
const { signal } = options;
|
|
14
|
+
if (signal?.aborted === true) {
|
|
15
|
+
await response.body.cancel().catch(() => void 0);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const reader = response.body.getReader();
|
|
19
|
+
const onAbort = () => {
|
|
20
|
+
reader.cancel().catch(() => void 0);
|
|
21
|
+
};
|
|
22
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
23
|
+
const decoder = new TextDecoder("utf-8");
|
|
24
|
+
let buffer = "";
|
|
25
|
+
let skipLineFeed = false;
|
|
26
|
+
let name = "";
|
|
27
|
+
let data = [];
|
|
28
|
+
let id;
|
|
29
|
+
let retry;
|
|
30
|
+
/** Apply one line; returns the event when this line completes one. */
|
|
31
|
+
const apply = (line) => {
|
|
32
|
+
if (line === "") {
|
|
33
|
+
const event = data.length === 0 ? void 0 : {
|
|
34
|
+
event: name === "" ? "message" : name,
|
|
35
|
+
data: data.join("\n"),
|
|
36
|
+
...id === void 0 ? {} : { id },
|
|
37
|
+
...retry === void 0 ? {} : { retry }
|
|
38
|
+
};
|
|
39
|
+
name = "";
|
|
40
|
+
data = [];
|
|
41
|
+
id = void 0;
|
|
42
|
+
retry = void 0;
|
|
43
|
+
return event;
|
|
44
|
+
}
|
|
45
|
+
if (line.startsWith(":")) return void 0;
|
|
46
|
+
const colon = line.indexOf(":");
|
|
47
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
48
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
49
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
50
|
+
if (field === "event") name = value;
|
|
51
|
+
else if (field === "data") data.push(value);
|
|
52
|
+
else if (field === "id") {
|
|
53
|
+
if (!value.includes("\0")) id = value;
|
|
54
|
+
} else if (field === "retry" && /^\d+$/u.test(value)) retry = Number(value);
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
let text = decoder.decode(value, { stream: true });
|
|
61
|
+
if (skipLineFeed && text.startsWith("\n")) text = text.slice(1);
|
|
62
|
+
if (text !== "") skipLineFeed = false;
|
|
63
|
+
buffer += text;
|
|
64
|
+
let start = 0;
|
|
65
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
66
|
+
const code = buffer.charCodeAt(index);
|
|
67
|
+
if (code !== 10 && code !== 13) continue;
|
|
68
|
+
const line = buffer.slice(start, index);
|
|
69
|
+
if (code === 13) if (index + 1 < buffer.length) {
|
|
70
|
+
if (buffer.charCodeAt(index + 1) === 10) index += 1;
|
|
71
|
+
} else skipLineFeed = true;
|
|
72
|
+
start = index + 1;
|
|
73
|
+
const event = apply(line);
|
|
74
|
+
if (event !== void 0) yield event;
|
|
75
|
+
}
|
|
76
|
+
buffer = buffer.slice(start);
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
signal?.removeEventListener("abort", onAbort);
|
|
80
|
+
await reader.cancel().catch(() => void 0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { readSSE };
|
package/lib/db/sqlite.d.ts
CHANGED
package/lib/db.d.ts
CHANGED
|
@@ -1,85 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/** Values supported by the portable SQL drivers. */
|
|
3
|
-
type SqlValue = null | number | bigint | string | Uint8Array;
|
|
4
|
-
/**
|
|
5
|
-
* Asynchronous SQL operations shared by a database and its transaction handles.
|
|
6
|
-
*
|
|
7
|
-
* `exec` runs SQL text without bindings and may contain several statements. `run`, `get`, and `all`
|
|
8
|
-
* execute one statement with positional bindings. Adapters may cache prepared statements by SQL text,
|
|
9
|
-
* so callers pass values as bindings instead of interpolating them. `run` resolves the number of rows
|
|
10
|
-
* the statement changed.
|
|
11
|
-
*/
|
|
12
|
-
interface DbExecutor {
|
|
13
|
-
exec(sql: string): Promise<void>;
|
|
14
|
-
run(sql: string, ...params: SqlValue[]): Promise<{
|
|
15
|
-
readonly changes: number;
|
|
16
|
-
}>;
|
|
17
|
-
get<T extends object>(sql: string, ...params: SqlValue[]): Promise<T | undefined>;
|
|
18
|
-
all<T extends object>(sql: string, ...params: SqlValue[]): Promise<T[]>;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Minimal portable SQL database facade.
|
|
22
|
-
*
|
|
23
|
-
* All operations are asynchronous so adapters may execute outside the caller's runtime.
|
|
24
|
-
*
|
|
25
|
-
* `transaction` passes the callback a transaction handle. All work in the transaction
|
|
26
|
-
* must use that handle; the handle is invalid after the callback settles. Adapters must
|
|
27
|
-
* queue unrelated operations and other transactions until the transaction finishes. The
|
|
28
|
-
* returned promise settles after commit or rollback. Calling the database itself (including
|
|
29
|
-
* `transaction`) from inside a callback therefore waits for that transaction and
|
|
30
|
-
* never settles.
|
|
31
|
-
*
|
|
32
|
-
* When the callback rejects, the adapter must roll the transaction back before rejecting
|
|
33
|
-
* with that same error. If rollback fails, it must reject with a different error (for
|
|
34
|
-
* example an `AggregateError`) so callers cannot mistake the callback error for a
|
|
35
|
-
* guaranteed rollback.
|
|
36
|
-
*
|
|
37
|
-
* Closing is the adapter's: the plugin that opened a database closes it, so the facade an
|
|
38
|
-
* app receives has no `close`.
|
|
39
|
-
*/
|
|
40
|
-
interface Db extends DbExecutor {
|
|
41
|
-
transaction<T>(callback: (transaction: DbExecutor) => Promise<T>): Promise<T>;
|
|
42
|
-
}
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region ../../plugins/acryl-db/src/facade/migrations.d.ts
|
|
45
|
-
type Migration = {
|
|
46
|
-
readonly version: number;
|
|
47
|
-
readonly statements: readonly string[];
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Apply all pending migrations atomically, tracking their version in the named metadata table.
|
|
51
|
-
*
|
|
52
|
-
* The bookkeeping SQL runs unchanged on SQLite and Postgres: no `STRICT` table, `ON CONFLICT DO NOTHING`
|
|
53
|
-
* instead of `INSERT OR IGNORE`, and the version written as a checked integer rather than a binding,
|
|
54
|
-
* since the two dialects spell placeholders differently.
|
|
55
|
-
*/
|
|
56
|
-
declare function applyMigrations(database: Db, migrations: readonly Migration[], options: {
|
|
57
|
-
readonly table: string;
|
|
58
|
-
}): Promise<void>;
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region ../../plugins/acryl-db/src/facade/schema.d.ts
|
|
61
|
-
/** Validate a prefix before using it as part of a SQL identifier. */
|
|
62
|
-
declare function validateSchemaPrefix(prefix: string): void;
|
|
63
|
-
//#endregion
|
|
64
|
-
//#region ../../plugins/acryl-db/src/migration-files.d.ts
|
|
65
|
-
/**
|
|
66
|
-
* Read every `NNN-name.sql` file of a folder, in number order.
|
|
67
|
-
* @throws when a `.sql` file is not named `NNN-name.sql`, or the numbers do not run 1, 2, 3, ... without gaps or repeats.
|
|
68
|
-
*/
|
|
69
|
-
declare function readMigrationFiles(folder: string | URL): Promise<Migration[]>;
|
|
70
|
-
//#endregion
|
|
71
|
-
//#region ../../plugins/acryl-db/src/index.d.ts
|
|
72
|
-
/**
|
|
73
|
-
* The `db` port: what an app's code sees of its relational database, whichever provider (`acryl-db-sqlite`, `acryl-db-postgres`) serves
|
|
74
|
-
* it. Plain code (an acrylblends app) opens a database with a provider's `openDb`; a Cordis plugin declares `inject: ['db']`, imports
|
|
75
|
-
* `acryl-db/cordis` for the `ctx.db` type, and never imports a driver.
|
|
76
|
-
*
|
|
77
|
-
* Bindings are positional `?` placeholders on every provider; a provider whose database spells them differently translates them.
|
|
78
|
-
* This entry imports nothing from Cordis, at runtime or in its types.
|
|
79
|
-
*/
|
|
80
|
-
/** The service name every `db` provider registers. */
|
|
81
|
-
declare const DB_SERVICE = "db";
|
|
82
|
-
/** The table that records an app's migration version. */
|
|
83
|
-
declare const MIGRATIONS_TABLE = "_migrations";
|
|
84
|
-
//#endregion
|
|
1
|
+
import { a as Migration, c as DbExecutor, i as validateSchemaPrefix, l as SqlValue, n as MIGRATIONS_TABLE, o as applyMigrations, r as readMigrationFiles, s as Db, t as DB_SERVICE } from "./index-D4FAsBZu.js";
|
|
85
2
|
export { DB_SERVICE, type Db, type DbExecutor, MIGRATIONS_TABLE, type Migration, type SqlValue, applyMigrations, readMigrationFiles, validateSchemaPrefix };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//#region ../../plugins/acryl-kv/src/kv.d.ts
|
|
2
|
+
/** `T`, except a promise: the callback of `update` is synchronous, and an async one is a type error. */
|
|
3
|
+
type NotPromise<T> = T extends PromiseLike<unknown> ? never : T;
|
|
4
|
+
/** Anything JSON can hold. */
|
|
5
|
+
type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
6
|
+
[key: string]: JsonValue;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Named JSON documents. Every call returns a promise, so a file, SQLite and Postgres answer alike.
|
|
10
|
+
*
|
|
11
|
+
* Keys are non-empty strings. Values are JSON: `undefined`, functions, symbols, bigints and non-finite numbers are refused with a
|
|
12
|
+
* `TypeError` before anything is written, and a value read back is a copy.
|
|
13
|
+
*
|
|
14
|
+
* There is no `close`: the code that opened a store closes it (see {@link KvOpened}), so a plugin cannot close the store another uses.
|
|
15
|
+
*/
|
|
16
|
+
interface Kv {
|
|
17
|
+
/** The value stored under the key, or `undefined`. */
|
|
18
|
+
get<T = JsonValue>(key: string): Promise<T | undefined>;
|
|
19
|
+
/** Store a value under the key, replacing any. Resolves once it is durable. */
|
|
20
|
+
set<T = JsonValue>(key: string, value: T): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Replace the value under the key by `change(current)`, atomically: `current` is `undefined` when the key is missing, `change` is
|
|
23
|
+
* synchronous (an `async` function is a type error, and a returned promise is rejected at run time: read what you need first, then update), and no other write to the key happens between reading it and writing the result. Resolves the new value. A `change` that
|
|
24
|
+
* throws, or returns something that is not JSON, leaves the value as it was and rejects with its error.
|
|
25
|
+
*/
|
|
26
|
+
update<T = JsonValue>(key: string, change: (current: T | undefined) => NotPromise<T>): Promise<T>;
|
|
27
|
+
/** Remove the key. A missing key is not an error. */
|
|
28
|
+
delete(key: string): Promise<void>;
|
|
29
|
+
/** The keys that start with the prefix (all of them without one), sorted by code point (the order of their UTF-8 bytes, not of UTF-16 units). */
|
|
30
|
+
list(prefix?: string): Promise<string[]>;
|
|
31
|
+
}
|
|
32
|
+
/** An open store and the one function that closes it. */
|
|
33
|
+
interface KvOpened {
|
|
34
|
+
readonly kv: Kv;
|
|
35
|
+
/** Waits for the writes in flight, then releases the storage. Calling it again does nothing; calls on `kv` after it reject. */
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ../../plugins/acryl-kv/src/check.d.ts
|
|
40
|
+
/**
|
|
41
|
+
* What every `kv` provider checks the same way, so a bad call fails alike on a file and on SQL: the key, the value, and the order of keys.
|
|
42
|
+
*/
|
|
43
|
+
/** A key is a non-empty string. */
|
|
44
|
+
declare function checkKey(key: unknown): asserts key is string;
|
|
45
|
+
/**
|
|
46
|
+
* The JSON text of a value. `undefined`, functions, symbols, bigints, circular structures and non-finite numbers (which JSON would turn
|
|
47
|
+
* into `null`) are refused with a `TypeError`; nothing is stored for them.
|
|
48
|
+
*/
|
|
49
|
+
declare function encodeValue(value: unknown): string;
|
|
50
|
+
/** The value of stored JSON text: a new copy each time, so changing what `get` returned changes nothing stored. */
|
|
51
|
+
declare function decodeValue<T>(text: string): T;
|
|
52
|
+
/** Order two keys by code point (the order of UTF-8 bytes, which SQLite's binary collation and Postgres' "C" collation give too). */
|
|
53
|
+
declare function compareKeys(a: string, b: string): number;
|
|
54
|
+
/** The number of code points in a string (what SQL's `substr` counts). */
|
|
55
|
+
declare function codePoints(text: string): number;
|
|
56
|
+
/**
|
|
57
|
+
* Call an `update` callback and check what it returns: the callback is synchronous, so a promise (an `async` function the types should have
|
|
58
|
+
* refused) is rejected with a message that says what to do, instead of being stored as `{}`.
|
|
59
|
+
*/
|
|
60
|
+
declare function runChange<T>(change: (current: T | undefined) => T, current: T | undefined): T;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region ../../plugins/acryl-kv/src/copy.d.ts
|
|
63
|
+
interface CopyKvOptions {
|
|
64
|
+
/** Replace keys the target already has. Without it, a shared key stops the copy before anything is written. */
|
|
65
|
+
readonly overwrite?: boolean;
|
|
66
|
+
/** Copy only the keys that start with this. */
|
|
67
|
+
readonly prefix?: string;
|
|
68
|
+
}
|
|
69
|
+
interface CopyKvResult {
|
|
70
|
+
/** Keys written to the target. */
|
|
71
|
+
readonly copied: number;
|
|
72
|
+
/** Of those, keys the target already had. */
|
|
73
|
+
readonly overwritten: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Copy every key of one store into another: the import when an app moves from a JSON file to SQL (the file opened with
|
|
77
|
+
* `acryl-kv-json` is the source, `kvOnDb` the target). The source is not changed.
|
|
78
|
+
* @throws when the target has a key the source has and `overwrite` is not set; nothing is written then.
|
|
79
|
+
*/
|
|
80
|
+
declare function copyKv(source: Kv, target: Kv, options?: CopyKvOptions): Promise<CopyKvResult>;
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region ../../plugins/acryl-kv/src/index.d.ts
|
|
83
|
+
/**
|
|
84
|
+
* The `kv` port: named JSON documents for an app too small for SQL, whichever provider serves it (`acryl-kv-json` for one file,
|
|
85
|
+
* `acryl-kv-db` over the `db` port). Plain code opens a store with a provider's `openKv`; a Cordis plugin declares `inject: ['kv']`, imports
|
|
86
|
+
* `acryl-kv/cordis` for the `ctx.kv` type, and never imports a provider.
|
|
87
|
+
*
|
|
88
|
+
* This entry imports nothing from Cordis, at runtime or in its types.
|
|
89
|
+
*/
|
|
90
|
+
/** The service name every `kv` provider registers. */
|
|
91
|
+
declare const KV_SERVICE = "kv";
|
|
92
|
+
//#endregion
|
|
93
|
+
export { checkKey as a, decodeValue as c, JsonValue as d, Kv as f, copyKv as i, encodeValue as l, NotPromise as m, CopyKvOptions as n, codePoints as o, KvOpened as p, CopyKvResult as r, compareKeys as s, KV_SERVICE as t, runChange as u };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region ../../plugins/acryl-db/src/facade/database.d.ts
|
|
2
|
+
/** Values supported by the portable SQL drivers. */
|
|
3
|
+
type SqlValue = null | number | bigint | string | Uint8Array;
|
|
4
|
+
/**
|
|
5
|
+
* Asynchronous SQL operations shared by a database and its transaction handles.
|
|
6
|
+
*
|
|
7
|
+
* `exec` runs SQL text without bindings and may contain several statements. `run`, `get`, and `all`
|
|
8
|
+
* execute one statement with positional bindings. Adapters may cache prepared statements by SQL text,
|
|
9
|
+
* so callers pass values as bindings instead of interpolating them. `run` resolves the number of rows
|
|
10
|
+
* the statement changed.
|
|
11
|
+
*/
|
|
12
|
+
interface DbExecutor {
|
|
13
|
+
exec(sql: string): Promise<void>;
|
|
14
|
+
run(sql: string, ...params: SqlValue[]): Promise<{
|
|
15
|
+
readonly changes: number;
|
|
16
|
+
}>;
|
|
17
|
+
get<T extends object>(sql: string, ...params: SqlValue[]): Promise<T | undefined>;
|
|
18
|
+
all<T extends object>(sql: string, ...params: SqlValue[]): Promise<T[]>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Minimal portable SQL database facade.
|
|
22
|
+
*
|
|
23
|
+
* All operations are asynchronous so adapters may execute outside the caller's runtime.
|
|
24
|
+
*
|
|
25
|
+
* `transaction` passes the callback a transaction handle. All work in the transaction
|
|
26
|
+
* must use that handle; the handle is invalid after the callback settles. Adapters must
|
|
27
|
+
* queue unrelated operations and other transactions until the transaction finishes. The
|
|
28
|
+
* returned promise settles after commit or rollback. Calling the database itself (including
|
|
29
|
+
* `transaction`) from inside a callback therefore waits for that transaction and
|
|
30
|
+
* never settles.
|
|
31
|
+
*
|
|
32
|
+
* When the callback rejects, the adapter must roll the transaction back before rejecting
|
|
33
|
+
* with that same error. If rollback fails, it must reject with a different error (for
|
|
34
|
+
* example an `AggregateError`) so callers cannot mistake the callback error for a
|
|
35
|
+
* guaranteed rollback.
|
|
36
|
+
*
|
|
37
|
+
* Closing is the adapter's: the plugin that opened a database closes it, so the facade an
|
|
38
|
+
* app receives has no `close`.
|
|
39
|
+
*/
|
|
40
|
+
interface Db extends DbExecutor {
|
|
41
|
+
transaction<T>(callback: (transaction: DbExecutor) => Promise<T>): Promise<T>;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region ../../plugins/acryl-db/src/facade/migrations.d.ts
|
|
45
|
+
type Migration = {
|
|
46
|
+
readonly version: number;
|
|
47
|
+
readonly statements: readonly string[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Apply all pending migrations atomically, tracking their version in the named metadata table.
|
|
51
|
+
*
|
|
52
|
+
* The bookkeeping SQL runs unchanged on SQLite and Postgres: no `STRICT` table, `ON CONFLICT DO NOTHING`
|
|
53
|
+
* instead of `INSERT OR IGNORE`, and the version written as a checked integer rather than a binding,
|
|
54
|
+
* since the two dialects spell placeholders differently.
|
|
55
|
+
*/
|
|
56
|
+
declare function applyMigrations(database: Db, migrations: readonly Migration[], options: {
|
|
57
|
+
readonly table: string;
|
|
58
|
+
}): Promise<void>;
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region ../../plugins/acryl-db/src/facade/schema.d.ts
|
|
61
|
+
/** Validate a prefix before using it as part of a SQL identifier. */
|
|
62
|
+
declare function validateSchemaPrefix(prefix: string): void;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../../plugins/acryl-db/src/migration-files.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Read every `NNN-name.sql` file of a folder, in number order.
|
|
67
|
+
* @throws when a `.sql` file is not named `NNN-name.sql`, or the numbers do not run 1, 2, 3, ... without gaps or repeats.
|
|
68
|
+
*/
|
|
69
|
+
declare function readMigrationFiles(folder: string | URL): Promise<Migration[]>;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region ../../plugins/acryl-db/src/index.d.ts
|
|
72
|
+
/**
|
|
73
|
+
* The `db` port: what an app's code sees of its relational database, whichever provider (`acryl-db-sqlite`, `acryl-db-postgres`) serves
|
|
74
|
+
* it. Plain code (an acrylblends app) opens a database with a provider's `openDb`; a Cordis plugin declares `inject: ['db']`, imports
|
|
75
|
+
* `acryl-db/cordis` for the `ctx.db` type, and never imports a driver.
|
|
76
|
+
*
|
|
77
|
+
* Bindings are positional `?` placeholders on every provider; a provider whose database spells them differently translates them.
|
|
78
|
+
* This entry imports nothing from Cordis, at runtime or in its types.
|
|
79
|
+
*/
|
|
80
|
+
/** The service name every `db` provider registers. */
|
|
81
|
+
declare const DB_SERVICE = "db";
|
|
82
|
+
/** The table that records an app's migration version. */
|
|
83
|
+
declare const MIGRATIONS_TABLE = "_migrations";
|
|
84
|
+
//#endregion
|
|
85
|
+
export { Migration as a, DbExecutor as c, validateSchemaPrefix as i, SqlValue as l, MIGRATIONS_TABLE as n, applyMigrations as o, readMigrationFiles as r, Db as s, DB_SERVICE as t };
|
package/lib/kv/db.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { s as Db } from "../index-D4FAsBZu.js";
|
|
2
|
+
import { f as Kv } from "../index-BLTYsyc1.js";
|
|
3
3
|
|
|
4
4
|
//#region ../../plugins/acryl-kv-db/src/open.d.ts
|
|
5
5
|
interface KvOnDbOptions {
|
package/lib/kv/json.d.ts
CHANGED