acrylblends 0.7.7 → 0.8.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 (195) hide show
  1. package/dist/index.d.ts +14 -6
  2. package/dist/index.js +130 -31
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +3 -3
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/agents/index.md +18 -3
  7. package/docs/framework/getting-started/basic.md +1 -2
  8. package/docs/framework/getting-started/desktop.md +3 -3
  9. package/docs/framework/guides/bring-your-own-tool.md +216 -0
  10. package/docs/framework/guides/client-modules.md +4 -3
  11. package/docs/framework/guides/create-acrylblends.md +5 -4
  12. package/docs/framework/guides/data.md +7 -6
  13. package/docs/framework/helpers/streaming.md +12 -27
  14. package/docs/framework/upstream.json +3 -3
  15. package/lib/browser/read-sse.js +84 -0
  16. package/lib/db/sqlite.d.ts +1 -1
  17. package/lib/db.d.ts +1 -84
  18. package/lib/index-BLTYsyc1.d.ts +93 -0
  19. package/lib/index-D4FAsBZu.d.ts +85 -0
  20. package/lib/kv/db.d.ts +2 -2
  21. package/lib/kv/json.d.ts +1 -1
  22. package/lib/kv/json.js +87 -24
  23. package/lib/kv.d.ts +1 -92
  24. package/lib/node-server.d.ts +3 -3
  25. package/lib/node-server.js +14 -8
  26. package/lib/read-sse.d.ts +33 -0
  27. package/lib/read-sse.js +84 -0
  28. package/package.json +5 -1
  29. package/plugins/acryl-db/plugin.json +7 -2
  30. package/plugins/acryl-db/src/conformance.ts +224 -0
  31. package/plugins/acryl-db/upstream.json +2 -2
  32. package/plugins/acryl-db-sqlite/plugin.json +4 -1
  33. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  34. package/plugins/acryl-door/LICENSE +47 -0
  35. package/plugins/acryl-door/plugin.json +14 -0
  36. package/plugins/acryl-door/src/cli/acryl.ts +207 -0
  37. package/plugins/acryl-door/src/host/api.ts +9 -0
  38. package/plugins/acryl-door/src/host/audit.ts +64 -0
  39. package/plugins/acryl-door/src/host/channel.ts +142 -0
  40. package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
  41. package/plugins/acryl-door/src/host/door-file.ts +94 -0
  42. package/plugins/acryl-door/src/host/door.ts +195 -0
  43. package/plugins/acryl-door/src/host/http.ts +80 -0
  44. package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
  45. package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
  46. package/plugins/acryl-door/src/host/page-routes.ts +65 -0
  47. package/plugins/acryl-door/src/host/shim.ts +65 -0
  48. package/plugins/acryl-door/src/host/verbs.ts +109 -0
  49. package/plugins/acryl-door/src/index.ts +133 -0
  50. package/plugins/acryl-door/src/page/api.ts +9 -0
  51. package/plugins/acryl-door/src/page/channel.ts +135 -0
  52. package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
  53. package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
  54. package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
  55. package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
  56. package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
  57. package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
  58. package/plugins/acryl-door/src/page/index.ts +40 -0
  59. package/plugins/acryl-door/src/page/indicator.ts +72 -0
  60. package/plugins/acryl-door/src/page/main.ts +7 -0
  61. package/plugins/acryl-door/src/page/protocol.ts +213 -0
  62. package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
  63. package/plugins/acryl-door/src/page/user-input.ts +24 -0
  64. package/plugins/acryl-door/upstream.json +28 -0
  65. package/plugins/acryl-inspect/upstream.json +1 -1
  66. package/plugins/acryl-kv/plugin.json +7 -2
  67. package/plugins/acryl-kv/src/conformance.ts +226 -0
  68. package/plugins/acryl-kv/upstream.json +1 -1
  69. package/plugins/acryl-kv-db/plugin.json +4 -1
  70. package/plugins/acryl-kv-db/upstream.json +1 -1
  71. package/plugins/acryl-kv-json/plugin.json +4 -1
  72. package/plugins/acryl-kv-json/src/open.ts +86 -22
  73. package/plugins/acryl-kv-json/upstream.json +2 -2
  74. package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
  75. package/plugins/acryl-node-server/src/client-modules.ts +10 -9
  76. package/plugins/acryl-node-server/upstream.json +2 -2
  77. package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
  78. package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
  79. package/plugins/acryl-router/src/service.ts +3 -0
  80. package/plugins/acryl-router/upstream.json +2 -2
  81. package/plugins/acryl-router-http/upstream.json +1 -1
  82. package/plugins/acryl-state/upstream.json +1 -1
  83. package/plugins/agent-panel/client/main.ts +159 -0
  84. package/plugins/agent-panel/index.ts +232 -0
  85. package/plugins/agent-panel/plugin.json +21 -0
  86. package/plugins/builder/index.ts +2 -2
  87. package/plugins/builder/plugin.json +1 -1
  88. package/plugins/desktop/main.ts +3 -3
  89. package/plugins/desktop/plugin.json +2 -2
  90. package/plugins/desktop/upstream.json +1 -1
  91. package/plugins/desktop-web/main.ts +2 -2
  92. package/plugins/desktop-web/plugin.json +2 -2
  93. package/plugins/desktop-web/upstream.json +1 -1
  94. package/plugins/extensions/index.ts +19 -1
  95. package/plugins/extensions/plugin.json +1 -1
  96. package/plugins/extensions/resolve.ts +17 -3
  97. package/plugins/manifest/lock.ts +7 -3
  98. package/plugins/manifest/upgrade-cli.ts +1 -1
  99. package/plugins/save/cli.ts +1 -1
  100. package/plugins/save/plugin.json +1 -1
  101. package/plugins/tsconfig.json +6 -0
  102. package/shared/AGENTS.core.md +10 -10
  103. package/shared/DEFAULTS.md +45 -0
  104. package/shared/SKILL.md +14 -0
  105. package/templates/agent/AGENTS.md +1 -1
  106. package/templates/agent/README.md +6 -6
  107. package/templates/agent/blend.yaml +3 -0
  108. package/templates/agent/gitignore +1 -0
  109. package/templates/agent/package.json +8 -8
  110. package/templates/agent/src/app.ts +46 -17
  111. package/templates/agent/src/main.ts +2 -2
  112. package/templates/agent/src/routes.ts +19 -6
  113. package/templates/agent/template.json +2 -1
  114. package/templates/agent/test/agent.test.ts +11 -5
  115. package/templates/agent/test/app.test.ts +30 -9
  116. package/templates/agent/test/builder.test.ts +35 -24
  117. package/templates/agent/test/model.test.ts +25 -20
  118. package/templates/agent/tsconfig.json +48 -50
  119. package/templates/blank/AGENTS.md +1 -1
  120. package/templates/blank/README.md +2 -2
  121. package/templates/blank/gitignore +1 -0
  122. package/templates/blank/package.json +8 -8
  123. package/templates/blank/src/about.tsx +2 -2
  124. package/templates/blank/src/app.ts +29 -9
  125. package/templates/blank/src/main.ts +1 -1
  126. package/templates/blank/src/routes.ts +1 -1
  127. package/templates/blank/test/app.test.ts +29 -10
  128. package/templates/blank/tsconfig.json +1 -1
  129. package/templates/chat/AGENTS.md +16 -0
  130. package/templates/chat/blend.yaml +29 -0
  131. package/templates/chat/gitignore +6 -0
  132. package/templates/chat/migrations/001-chat.sql +12 -0
  133. package/templates/chat/package.json +33 -0
  134. package/templates/chat/src/app.ts +106 -0
  135. package/templates/chat/src/chat-app.ts +101 -0
  136. package/templates/chat/src/client/chat.tsx +115 -0
  137. package/templates/chat/src/client/tsconfig.json +26 -0
  138. package/templates/chat/src/db.ts +43 -0
  139. package/templates/chat/src/door.ts +7 -0
  140. package/templates/chat/src/llm.ts +42 -0
  141. package/templates/chat/src/main.ts +28 -0
  142. package/templates/chat/src/page.tsx +40 -0
  143. package/templates/chat/src/routes.ts +15 -0
  144. package/templates/chat/src/runtime.ts +17 -0
  145. package/templates/chat/template.json +11 -0
  146. package/templates/chat/test/chat.test.ts +198 -0
  147. package/templates/chat/test/server.test.ts +78 -0
  148. package/templates/chat/tsconfig.json +20 -0
  149. package/templates/nodejs/AGENTS.md +9 -0
  150. package/templates/nodejs/blend.yaml +26 -0
  151. package/templates/nodejs/gitignore +3 -0
  152. package/templates/nodejs/package.json +30 -0
  153. package/templates/nodejs/src/app.ts +92 -0
  154. package/templates/nodejs/src/client/counter.tsx +12 -0
  155. package/templates/nodejs/src/client/tsconfig.json +26 -0
  156. package/templates/nodejs/src/door.ts +7 -0
  157. package/templates/nodejs/src/main.ts +25 -0
  158. package/templates/nodejs/src/page.tsx +29 -0
  159. package/templates/nodejs/src/routes.ts +40 -0
  160. package/templates/nodejs/template.json +3 -0
  161. package/templates/nodejs/test/app.test.ts +78 -0
  162. package/templates/nodejs/test/server.test.ts +83 -0
  163. package/templates/nodejs/tsconfig.json +20 -0
  164. package/templates/notes/AGENTS.md +26 -0
  165. package/templates/notes/blend.yaml +29 -0
  166. package/templates/notes/gitignore +6 -0
  167. package/templates/notes/migrations/001-notes.sql +18 -0
  168. package/templates/notes/package.json +33 -0
  169. package/templates/notes/src/app.ts +106 -0
  170. package/templates/notes/src/auth.ts +57 -0
  171. package/templates/notes/src/client/counter.tsx +21 -0
  172. package/templates/notes/src/client/tsconfig.json +26 -0
  173. package/templates/notes/src/db.ts +50 -0
  174. package/templates/notes/src/door.ts +7 -0
  175. package/templates/notes/src/main.ts +28 -0
  176. package/templates/notes/src/notes-app.ts +205 -0
  177. package/templates/notes/src/routes.ts +14 -0
  178. package/templates/notes/src/runtime.ts +17 -0
  179. package/templates/notes/src/schema.ts +38 -0
  180. package/templates/notes/src/views.tsx +136 -0
  181. package/templates/notes/template.json +11 -0
  182. package/templates/notes/test/notes.test.ts +280 -0
  183. package/templates/notes/test/server.test.ts +78 -0
  184. package/templates/notes/tsconfig.json +20 -0
  185. package/templates/todo/README.md +1 -1
  186. package/templates/todo/gitignore +1 -0
  187. package/templates/todo/package.json +8 -8
  188. package/templates/todo/src/app.ts +32 -12
  189. package/templates/todo/src/main.ts +1 -1
  190. package/templates/todo/src/todo.ts +4 -4
  191. package/templates/todo/test/todo.test.ts +48 -23
  192. package/templates/todo/tsconfig.json +1 -1
  193. package/templates/todo/upstream.json +2 -2
  194. package/lib/database-D_UN6We8.d.ts +0 -44
  195. package/lib/kv-DWDm0Lja.d.ts +0 -39
@@ -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 { t as Db } from "../database-D_UN6We8.js";
2
- import { t as Kv } from "../kv-DWDm0Lja.js";
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
@@ -1,4 +1,4 @@
1
- import { n as KvOpened, t as Kv } from "../kv-DWDm0Lja.js";
1
+ import { f as Kv, p as KvOpened } from "../index-BLTYsyc1.js";
2
2
 
3
3
  //#region ../../plugins/acryl-kv-json/src/open.d.ts
4
4
  interface OpenKvOptions {
package/lib/kv/json.js CHANGED
@@ -7,9 +7,12 @@ import { basename, dirname, join, resolve } from "node:path";
7
7
  * memory and resolves the `kv` port with the function that closes it. The Cordis plugin is a thin wrapper over this, so both run one code
8
8
  * path. This entry imports nothing from Cordis.
9
9
  *
10
- * Every write queues behind the writes before it and persists the whole map: serialize to a temporary file in the same folder, flush it,
11
- * rename it over the file, and flush the folder where the platform allows. The call resolves after the rename, so a crash leaves the old file
12
- * or the new one, never half of one. One process owns the file: two writers would overwrite each other.
10
+ * Every write persists the whole map: serialize to a temporary file in the same folder, flush it, rename it over the file, and flush the
11
+ * folder where the platform allows. The call resolves after the rename, so a crash leaves the old file or the new one, never half of one.
12
+ * Writes are grouped: the calls made in one turn of the event loop, and those that queue while a file write is in flight, are applied in
13
+ * call order and persisted together by one write, so a burst of calls costs one flush and one rename, not one each (on Windows each costs
14
+ * tens of milliseconds). One process owns the
15
+ * file: two writers would overwrite each other.
13
16
  */
14
17
  /** The file's own format version, for a later change of layout. */
15
18
  const FORMAT = 1;
@@ -80,7 +83,12 @@ async function openKv(options) {
80
83
  let state = await load(file);
81
84
  let closed = false;
82
85
  let sequence = 0;
83
- let tail = Promise.resolve();
86
+ /** Calls waiting for the next file write, in call order. */
87
+ let queue = [];
88
+ /** Whether the loop that writes the queue runs; cleared by the loop itself, so a call queued after it ends starts a new one. */
89
+ let draining = false;
90
+ /** The latest loop, for `close` to wait on. */
91
+ let drained = Promise.resolve();
84
92
  const guard = () => {
85
93
  if (closed) throw new Error(`acryl-kv-json: ${file} is closed`);
86
94
  };
@@ -103,18 +111,66 @@ async function openKv(options) {
103
111
  }
104
112
  await flushFolder(dirname(file));
105
113
  };
106
- /** Run a change after every earlier one; the map it works on becomes the state only once it is on disk. */
107
- const write = (change) => {
108
- guard();
109
- const task = tail.then(async () => {
114
+ /**
115
+ * Write the queue until it is empty. Each round takes every call queued so far and applies them in order to one copy of the state. A call
116
+ * whose change throws rejects with its error and changes nothing, and the others in the round still apply. The copy becomes the state
117
+ * only once it is on disk. A failed file write rejects every call in its round and leaves the state as it was.
118
+ */
119
+ const drain = async () => {
120
+ try {
121
+ await Promise.resolve();
122
+ await rounds();
123
+ } finally {
124
+ draining = false;
125
+ }
126
+ };
127
+ const rounds = async () => {
128
+ while (queue.length > 0) {
129
+ const round = queue;
130
+ queue = [];
110
131
  const draft = new Map(state);
111
- const result = change(draft);
112
- await persist(draft);
132
+ const applied = [];
133
+ for (const call of round) {
134
+ let change;
135
+ try {
136
+ change = call.change((key) => draft.get(key));
137
+ } catch (error) {
138
+ call.reject(error);
139
+ continue;
140
+ }
141
+ if (change.text === void 0) draft.delete(change.key);
142
+ else draft.set(change.key, change.text);
143
+ applied.push({
144
+ call,
145
+ result: change.result
146
+ });
147
+ }
148
+ if (applied.length === 0) continue;
149
+ try {
150
+ await persist(draft);
151
+ } catch (error) {
152
+ for (const { call } of applied) call.reject(error);
153
+ continue;
154
+ }
113
155
  state = draft;
114
- return result;
156
+ for (const { call, result } of applied) call.resolve(result);
157
+ }
158
+ };
159
+ /** Queue one change behind every earlier call; it resolves once a file write that contains it is on disk. */
160
+ const write = (change) => {
161
+ guard();
162
+ const done = new Promise((resolveCall, rejectCall) => {
163
+ queue.push({
164
+ change,
165
+ resolve: resolveCall,
166
+ reject: rejectCall
167
+ });
115
168
  });
116
- tail = task.catch(() => void 0);
117
- return task;
169
+ if (!draining) {
170
+ draining = true;
171
+ drained = drain();
172
+ }
173
+ return done;
118
174
  };
119
175
  return {
120
176
  kv: {
@@ -127,24 +183,31 @@ async function openKv(options) {
127
183
  async set(key, value) {
128
184
  checkKey(key);
129
185
  const text = encodeValue(value);
130
- await write((draft) => {
131
- draft.set(key, text);
132
- });
186
+ await write(() => ({
187
+ key,
188
+ text,
189
+ result: void 0
190
+ }));
133
191
  },
134
192
  async update(key, change) {
135
193
  checkKey(key);
136
- return write((draft) => {
137
- const text = draft.get(key);
194
+ return write((read) => {
195
+ const text = read(key);
138
196
  const encoded = encodeValue(runChange(change, text === void 0 ? void 0 : decodeValue(text)));
139
- draft.set(key, encoded);
140
- return decodeValue(encoded);
197
+ return {
198
+ key,
199
+ text: encoded,
200
+ result: decodeValue(encoded)
201
+ };
141
202
  });
142
203
  },
143
204
  async delete(key) {
144
205
  checkKey(key);
145
- await write((draft) => {
146
- draft.delete(key);
147
- });
206
+ await write(() => ({
207
+ key,
208
+ text: void 0,
209
+ result: void 0
210
+ }));
148
211
  },
149
212
  async list(prefix = "") {
150
213
  guard();
@@ -153,7 +216,7 @@ async function openKv(options) {
153
216
  },
154
217
  close: async () => {
155
218
  closed = true;
156
- await tail;
219
+ await drained;
157
220
  }
158
221
  };
159
222
  }
package/lib/kv.d.ts CHANGED
@@ -1,93 +1,2 @@
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
1
+ import { a as checkKey, c as decodeValue, d as JsonValue, f as Kv, i as copyKv, l as encodeValue, m as NotPromise, n as CopyKvOptions, o as codePoints, p as KvOpened, r as CopyKvResult, s as compareKeys, t as KV_SERVICE, u as runChange } from "./index-BLTYsyc1.js";
93
2
  export { type CopyKvOptions, type CopyKvResult, type JsonValue, KV_SERVICE, type Kv, type KvOpened, type NotPromise, checkKey, codePoints, compareKeys, copyKv, decodeValue, encodeValue, runChange };
@@ -136,7 +136,7 @@ interface ClientModulesOptions {
136
136
  readonly compiled?: string | undefined;
137
137
  /** At most this many bytes of compiled modules are kept in memory (least recently served first out); 16 MiB when not given. */
138
138
  readonly cacheBytes?: number | undefined;
139
- /** The folder holding `jsx-dom.js` and `client.js`; the framework's own when not given. */
139
+ /** The folder holding `jsx-dom.js`, `client.js` and `read-sse.js`; the framework's own when not given. */
140
140
  readonly runtimeDir?: string | undefined;
141
141
  }
142
142
  /** Options of {@link buildClientModules}. */
@@ -145,7 +145,7 @@ interface BuildClientModulesOptions {
145
145
  readonly root: string;
146
146
  /** Where the compiled modules go, at the same relative paths and file names; it must not lie inside `root`. */
147
147
  readonly outDir: string;
148
- /** The folder holding `jsx-dom.js` and `client.js`; the framework's own when not given. */
148
+ /** The folder holding `jsx-dom.js`, `client.js` and `read-sse.js`; the framework's own when not given. */
149
149
  readonly runtimeDir?: string | undefined;
150
150
  }
151
151
  /**
@@ -159,7 +159,7 @@ interface BuildClientModulesOptions {
159
159
  declare function clientModules(options: ClientModulesOptions): Acryl;
160
160
  /**
161
161
  * Compile a folder of client modules once, for a production that has no `typescript`: each module is written to `outDir` at the same
162
- * relative path and file name (so the page's URLs stay the same), compiled exactly as {@link clientModules} compiles it, and the two
162
+ * relative path and file name (so the page's URLs stay the same), compiled exactly as {@link clientModules} compiles it, and the three
163
163
  * browser files are copied to `outDir/@acrylblends/`. Files already in `outDir` that no source produces are left there.
164
164
  * @param options - the source folder and the output folder.
165
165
  * @returns the paths written, relative to `outDir`.
@@ -835,6 +835,7 @@ const drainIncoming = (incoming) => {
835
835
  bytesRead += chunk.length;
836
836
  if (bytesRead > MAX_DRAIN_BYTES) forceClose();
837
837
  };
838
+ incoming.removeAllListeners("data");
838
839
  incoming.on("data", onData);
839
840
  incoming.on("end", cleanup);
840
841
  incoming.on("error", cleanup);
@@ -1330,14 +1331,14 @@ function serve(options, listeningListener) {
1330
1331
  /**
1331
1332
  * Client components without a bundler. `clientModules({ root: 'src/client' })` is an app that serves the TypeScript and JSX modules of a
1332
1333
  * folder to the browser as ES modules: a `.tsx`/`.ts`/`.jsx` file compiled with the app's own `typescript` (JSX for `acrylblends/jsx/dom`),
1333
- * a `.js`/`.mjs` file as it is. The framework's own browser imports (`acrylblends/jsx/dom` with its runtimes and `client`, and
1334
- * `acrylblends/client`) are rewritten to two self-contained files the same app serves, at a URL relative to each module, so a page needs
1334
+ * a `.js`/`.mjs` file as it is. The framework's own browser imports (`acrylblends/jsx/dom` with its runtimes and `client`,
1335
+ * `acrylblends/client` and `acrylblends/read-sse`) are rewritten to three self-contained files the same app serves, at a URL relative to each module, so a page needs
1335
1336
  * one `<script type="module" src="/client/counter.tsx">` and no import map, under whatever path the app is routed at.
1336
1337
  *
1337
1338
  * Production runs without `typescript` (a deploy that leaves out devDependencies): `buildClientModules` writes the same compiled modules
1338
- * and the two browser files into a folder, and with `NODE_ENV=production` and `compiled` set, only that folder is served.
1339
+ * and the three browser files into a folder, and with `NODE_ENV=production` and `compiled` set, only that folder is served.
1339
1340
  */
1340
- /** The folder the two browser files are served from, below the app's mount. A source folder of this name is not served. */
1341
+ /** The folder the three browser files are served from, below the app's mount. A source folder of this name is not served. */
1341
1342
  const RUNTIME_DIR = "@acrylblends";
1342
1343
  /** The framework's browser imports and the browser file that provides each. */
1343
1344
  const BROWSER_IMPORTS = {
@@ -1345,9 +1346,14 @@ const BROWSER_IMPORTS = {
1345
1346
  "acrylblends/jsx/dom/jsx-runtime": "jsx-dom.js",
1346
1347
  "acrylblends/jsx/dom/jsx-dev-runtime": "jsx-dom.js",
1347
1348
  "acrylblends/jsx/dom/client": "jsx-dom.js",
1348
- "acrylblends/client": "client.js"
1349
+ "acrylblends/client": "client.js",
1350
+ "acrylblends/read-sse": "read-sse.js"
1349
1351
  };
1350
- const RUNTIME_FILES = ["jsx-dom.js", "client.js"];
1352
+ const RUNTIME_FILES = [
1353
+ "jsx-dom.js",
1354
+ "client.js",
1355
+ "read-sse.js"
1356
+ ];
1351
1357
  const COMPILED = /* @__PURE__ */ new Set([
1352
1358
  ".ts",
1353
1359
  ".tsx",
@@ -1362,7 +1368,7 @@ const SERVED = /* @__PURE__ */ new Set([
1362
1368
  const JS_TYPE = "text/javascript; charset=utf-8";
1363
1369
  /** The cache bound when `cacheBytes` is not given. */
1364
1370
  const DEFAULT_CACHE_BYTES = 16 * 1024 * 1024;
1365
- /** The framework's two browser files, next to the built module that contains this code. */
1371
+ /** The framework's three browser files, next to the built module that contains this code. */
1366
1372
  const defaultRuntimeDir = () => fileURLToPath(new URL("./browser/", import.meta.url));
1367
1373
  /** A module path below the root (`widgets/counter.tsx`) with the URL prefix that reaches the browser files from it. */
1368
1374
  const runtimePrefix = (relativePath) => {
@@ -1551,7 +1557,7 @@ function sourceFiles(root, dir = root) {
1551
1557
  }
1552
1558
  /**
1553
1559
  * Compile a folder of client modules once, for a production that has no `typescript`: each module is written to `outDir` at the same
1554
- * relative path and file name (so the page's URLs stay the same), compiled exactly as {@link clientModules} compiles it, and the two
1560
+ * relative path and file name (so the page's URLs stay the same), compiled exactly as {@link clientModules} compiles it, and the three
1555
1561
  * browser files are copied to `outDir/@acrylblends/`. Files already in `outDir` that no source produces are left there.
1556
1562
  * @param options - the source folder and the output folder.
1557
1563
  * @returns the paths written, relative to `outDir`.
@@ -0,0 +1,33 @@
1
+ //#region ../../plugins/acryl-router/src/helpers/read-sse.d.ts
2
+ /**
3
+ * `readSSE(response)`: the events of a `text/event-stream` answer from `fetch`, for the browser. `EventSource` reads GET streams only; a chat
4
+ * or any stream that needs a POST body, a header or an `AbortSignal` is read with `fetch`, and this is the loop every such page otherwise writes by
5
+ * hand. It follows the parsing rules of the HTML standard (lines end in LF, CRLF or CR; the `data` lines of one event join with LF; a line that
6
+ * starts with a colon is a comment; an event ends at a blank line; an event with no `data` line is not dispatched; the last event is dispatched
7
+ * only if its blank line arrived), and it is one module with no imports, so it also runs in a service worker and on a fetch runtime.
8
+ *
9
+ * ACRYL's own addition, not part of the upstream port.
10
+ */
11
+ /** One event of the stream. */
12
+ interface SSEEvent {
13
+ /** The `event:` name; `message` when the event has none. */
14
+ readonly event: string;
15
+ /** The `data:` lines of the event, joined with a line feed. */
16
+ readonly data: string;
17
+ /** The `id:` of the event, when it had one. */
18
+ readonly id?: string;
19
+ /** The `retry:` of the event in milliseconds, when it had one. */
20
+ readonly retry?: number;
21
+ }
22
+ interface ReadSSEOptions {
23
+ /** Stops the loop and cancels the response body when it aborts. (Abort the `fetch` itself with the signal you pass to it.) */
24
+ readonly signal?: AbortSignal;
25
+ }
26
+ /**
27
+ * 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
28
+ * request to the server ends.
29
+ * @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.
30
+ */
31
+ declare function readSSE(response: Response, options?: ReadSSEOptions): AsyncGenerator<SSEEvent, void, undefined>;
32
+ //#endregion
33
+ export { ReadSSEOptions, SSEEvent, readSSE };
@@ -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 };