acrylblends 0.7.6 → 0.7.7

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 (48) hide show
  1. package/dist/index.js +89 -5
  2. package/dist/index.js.map +1 -1
  3. package/docs/framework/UPSTREAM.md +1 -1
  4. package/docs/framework/guides/data.md +66 -2
  5. package/docs/framework/upstream.json +2 -2
  6. package/lib/database-D_UN6We8.d.ts +44 -0
  7. package/lib/db/sqlite.d.ts +1 -1
  8. package/lib/db.d.ts +84 -1
  9. package/lib/kv/db.d.ts +12 -0
  10. package/lib/kv/db.js +57 -0
  11. package/lib/kv/json.d.ts +11 -0
  12. package/lib/kv/json.js +161 -0
  13. package/lib/kv-DWDm0Lja.d.ts +39 -0
  14. package/lib/kv.d.ts +93 -0
  15. package/lib/kv.js +2 -0
  16. package/lib/src-DRFVEQQb.js +90 -0
  17. package/package.json +13 -1
  18. package/plugins/acryl-db/upstream.json +1 -1
  19. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  20. package/plugins/acryl-inspect/upstream.json +1 -1
  21. package/plugins/acryl-kv/LICENSE +47 -0
  22. package/plugins/acryl-kv/plugin.json +12 -0
  23. package/plugins/acryl-kv/src/check.ts +58 -0
  24. package/plugins/acryl-kv/src/copy.ts +39 -0
  25. package/plugins/acryl-kv/src/cordis.ts +13 -0
  26. package/plugins/acryl-kv/src/index.ts +14 -0
  27. package/plugins/acryl-kv/src/kv.ts +37 -0
  28. package/plugins/acryl-kv/upstream.json +18 -0
  29. package/plugins/acryl-kv-db/LICENSE +47 -0
  30. package/plugins/acryl-kv-db/plugin.json +13 -0
  31. package/plugins/acryl-kv-db/src/index.ts +37 -0
  32. package/plugins/acryl-kv-db/src/open.ts +80 -0
  33. package/plugins/acryl-kv-db/upstream.json +21 -0
  34. package/plugins/acryl-kv-json/LICENSE +47 -0
  35. package/plugins/acryl-kv-json/plugin.json +13 -0
  36. package/plugins/acryl-kv-json/src/index.ts +71 -0
  37. package/plugins/acryl-kv-json/src/open.ts +185 -0
  38. package/plugins/acryl-kv-json/upstream.json +22 -0
  39. package/plugins/acryl-node-server/src/index.ts +12 -1
  40. package/plugins/acryl-node-server/upstream.json +2 -2
  41. package/plugins/acryl-router/upstream.json +1 -1
  42. package/plugins/acryl-router-http/upstream.json +1 -1
  43. package/plugins/acryl-state/upstream.json +1 -1
  44. package/plugins/desktop/upstream.json +1 -1
  45. package/plugins/desktop-web/upstream.json +1 -1
  46. package/plugins/tsconfig.json +2 -0
  47. package/templates/todo/upstream.json +1 -1
  48. package/lib/index-D4FAsBZu.d.ts +0 -85
@@ -0,0 +1,44 @@
1
+ //#region ../../plugins/acryl-db/lib/types/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
+ export { DbExecutor as n, SqlValue as r, Db as t };
@@ -1,4 +1,4 @@
1
- import { c as DbExecutor, l as SqlValue, s as Db } from "../index-D4FAsBZu.js";
1
+ import { n as DbExecutor, r as SqlValue, t as Db } from "../database-D_UN6We8.js";
2
2
 
3
3
  //#region ../../plugins/acryl-db-sqlite/src/open.d.ts
4
4
  interface OpenDbOptions {
package/lib/db.d.ts CHANGED
@@ -1,2 +1,85 @@
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";
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
2
85
  export { DB_SERVICE, type Db, type DbExecutor, MIGRATIONS_TABLE, type Migration, type SqlValue, applyMigrations, readMigrationFiles, validateSchemaPrefix };
package/lib/kv/db.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { t as Db } from "../database-D_UN6We8.js";
2
+ import { t as Kv } from "../kv-DWDm0Lja.js";
3
+
4
+ //#region ../../plugins/acryl-kv-db/src/open.d.ts
5
+ interface KvOnDbOptions {
6
+ /** The table that holds the documents. Defaults to `kv`; a plain SQL identifier. */
7
+ readonly table?: string;
8
+ }
9
+ /** The `kv` port over a `db`. It does not own the database: whoever opened it closes it, and calls after that reject. */
10
+ declare function kvOnDb(db: Db, options?: KvOnDbOptions): Kv;
11
+ //#endregion
12
+ export { type Kv, KvOnDbOptions, kvOnDb };
package/lib/kv/db.js ADDED
@@ -0,0 +1,57 @@
1
+ import { a as compareKeys, c as runChange, i as codePoints, o as decodeValue, r as checkKey, s as encodeValue } from "../src-DRFVEQQb.js";
2
+ //#region ../../plugins/acryl-kv-db/src/open.ts
3
+ const IDENTIFIER = /^[a-z_][a-z0-9_]*$/u;
4
+ /** The `kv` port over a `db`. It does not own the database: whoever opened it closes it, and calls after that reject. */
5
+ function kvOnDb(db, options = {}) {
6
+ const table = options.table ?? "kv";
7
+ if (!IDENTIFIER.test(table)) throw new TypeError(`acryl-kv-db: the table name must be a plain lower case SQL identifier, got ${JSON.stringify(table)}`);
8
+ let ready;
9
+ const ensure = () => {
10
+ ready ??= db.exec(`CREATE TABLE IF NOT EXISTS ${table} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`).catch((error) => {
11
+ ready = void 0;
12
+ throw error;
13
+ });
14
+ return ready;
15
+ };
16
+ /**
17
+ * Run a statement after the table exists. The table's statement and this one are both handed to the database at once (its queue keeps
18
+ * their order), so a `close()` called right after a write cannot slip between them and leave the write on a closed database.
19
+ */
20
+ const after = async (work) => {
21
+ const created = ensure();
22
+ const result = work();
23
+ const [, value] = await Promise.all([created, result]);
24
+ return value;
25
+ };
26
+ const upsert = `INSERT INTO ${table} (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value`;
27
+ return {
28
+ async get(key) {
29
+ checkKey(key);
30
+ const row = await after(() => db.get(`SELECT value FROM ${table} WHERE key = ?`, key));
31
+ return row === void 0 ? void 0 : decodeValue(row.value);
32
+ },
33
+ async set(key, value) {
34
+ checkKey(key);
35
+ const text = encodeValue(value);
36
+ await after(() => db.run(upsert, key, text));
37
+ },
38
+ async update(key, change) {
39
+ checkKey(key);
40
+ return after(() => db.transaction(async (tx) => {
41
+ const row = await tx.get(`SELECT value FROM ${table} WHERE key = ?`, key);
42
+ const text = encodeValue(runChange(change, row === void 0 ? void 0 : decodeValue(row.value)));
43
+ await tx.run(upsert, key, text);
44
+ return decodeValue(text);
45
+ }));
46
+ },
47
+ async delete(key) {
48
+ checkKey(key);
49
+ await after(() => db.run(`DELETE FROM ${table} WHERE key = ?`, key));
50
+ },
51
+ async list(prefix = "") {
52
+ return (await after(() => prefix === "" ? db.all(`SELECT key FROM ${table}`) : db.all(`SELECT key FROM ${table} WHERE substr(key, 1, ?) = ?`, codePoints(prefix), prefix))).map((row) => row.key).sort(compareKeys);
53
+ }
54
+ };
55
+ }
56
+ //#endregion
57
+ export { kvOnDb };
@@ -0,0 +1,11 @@
1
+ import { n as KvOpened, t as Kv } from "../kv-DWDm0Lja.js";
2
+
3
+ //#region ../../plugins/acryl-kv-json/src/open.d.ts
4
+ interface OpenKvOptions {
5
+ /** The JSON file; its folder is created if missing. */
6
+ readonly path: string;
7
+ }
8
+ /** Open (and create, when missing) a JSON-file store. @throws when the file exists and is not a readable kv file. */
9
+ declare function openKv(options: OpenKvOptions): Promise<KvOpened>;
10
+ //#endregion
11
+ export { type Kv, type KvOpened, OpenKvOptions, openKv };
package/lib/kv/json.js ADDED
@@ -0,0 +1,161 @@
1
+ import { a as compareKeys, c as runChange, o as decodeValue, r as checkKey, s as encodeValue } from "../src-DRFVEQQb.js";
2
+ import { mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ //#region ../../plugins/acryl-kv-json/src/open.ts
5
+ /**
6
+ * The plain way to open the JSON-file provider, for code without Cordis (an acrylblends app): `openKv({ path })` reads one JSON file into
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
+ * path. This entry imports nothing from Cordis.
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.
13
+ */
14
+ /** The file's own format version, for a later change of layout. */
15
+ const FORMAT = 1;
16
+ /** Stray temporary files from a crash end in `.tmp` and start with the file's name. */
17
+ const tempPrefix = (file) => `${basename(file)}.`;
18
+ /** The file content for a map of key to JSON text, keys in code point order, so the same data is the same bytes. */
19
+ function serialize(entries) {
20
+ const body = [...entries.keys()].sort(compareKeys).map((key) => `${JSON.stringify(key)}:${entries.get(key)}`).join(",");
21
+ return `{"version":${FORMAT},"entries":{${body}}}\n`;
22
+ }
23
+ function unreadable(file, reason) {
24
+ return /* @__PURE__ */ new Error(`acryl-kv-json: ${file} is not a readable kv file (${reason}); it was not changed`);
25
+ }
26
+ /** The entries of a file, or none when it does not exist. A file that exists and does not parse is an error: it is never started over. */
27
+ async function load(file) {
28
+ let text;
29
+ try {
30
+ text = await readFile(file, "utf8");
31
+ } catch (error) {
32
+ if (error.code === "ENOENT") return /* @__PURE__ */ new Map();
33
+ throw error;
34
+ }
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(text);
38
+ } catch (error) {
39
+ throw unreadable(file, error.message);
40
+ }
41
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== FORMAT) throw unreadable(file, "not a version 1 kv file");
42
+ const entries = parsed.entries;
43
+ if (typeof entries !== "object" || entries === null || Array.isArray(entries)) throw unreadable(file, "no entries object");
44
+ return new Map(Object.entries(entries).map(([key, value]) => [key, JSON.stringify(value)]));
45
+ }
46
+ /** Remove the temporary files an earlier crash left beside the file. */
47
+ async function removeStrays(file) {
48
+ const prefix = tempPrefix(file);
49
+ for (const name of await readdir(dirname(file))) if (name.startsWith(prefix) && name.endsWith(".tmp")) await rm(join(dirname(file), name), { force: true });
50
+ }
51
+ const RENAME_RETRIES = 8;
52
+ /** Rename over the target. Windows refuses while a scanner or an indexer holds the target for a moment, so it is retried briefly. */
53
+ async function replace(from, to) {
54
+ for (let attempt = 0;; attempt += 1) try {
55
+ await rename(from, to);
56
+ return;
57
+ } catch (error) {
58
+ const code = error.code;
59
+ if (attempt >= RENAME_RETRIES || code !== "EPERM" && code !== "EBUSY" && code !== "EACCES") throw error;
60
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10 * (attempt + 1)));
61
+ }
62
+ }
63
+ /** Flush the folder entry of a renamed file (POSIX); Windows cannot open a folder, and a filesystem that refuses is no worse than before. */
64
+ async function flushFolder(folder) {
65
+ if (process.platform === "win32") return;
66
+ try {
67
+ const handle = await open(folder, "r");
68
+ try {
69
+ await handle.sync();
70
+ } finally {
71
+ await handle.close();
72
+ }
73
+ } catch {}
74
+ }
75
+ /** Open (and create, when missing) a JSON-file store. @throws when the file exists and is not a readable kv file. */
76
+ async function openKv(options) {
77
+ const file = resolve(options.path);
78
+ await mkdir(dirname(file), { recursive: true });
79
+ await removeStrays(file);
80
+ let state = await load(file);
81
+ let closed = false;
82
+ let sequence = 0;
83
+ let tail = Promise.resolve();
84
+ const guard = () => {
85
+ if (closed) throw new Error(`acryl-kv-json: ${file} is closed`);
86
+ };
87
+ /** Persist a map: the file is the old one or this one, never between. */
88
+ const persist = async (next) => {
89
+ sequence += 1;
90
+ const temp = join(dirname(file), `${tempPrefix(file)}${process.pid}.${sequence}.tmp`);
91
+ try {
92
+ const handle = await open(temp, "w");
93
+ try {
94
+ await handle.writeFile(serialize(next), "utf8");
95
+ await handle.sync();
96
+ } finally {
97
+ await handle.close();
98
+ }
99
+ await replace(temp, file);
100
+ } catch (error) {
101
+ await rm(temp, { force: true });
102
+ throw error;
103
+ }
104
+ await flushFolder(dirname(file));
105
+ };
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 () => {
110
+ const draft = new Map(state);
111
+ const result = change(draft);
112
+ await persist(draft);
113
+ state = draft;
114
+ return result;
115
+ });
116
+ tail = task.catch(() => void 0);
117
+ return task;
118
+ };
119
+ return {
120
+ kv: {
121
+ async get(key) {
122
+ guard();
123
+ checkKey(key);
124
+ const text = state.get(key);
125
+ return text === void 0 ? void 0 : decodeValue(text);
126
+ },
127
+ async set(key, value) {
128
+ checkKey(key);
129
+ const text = encodeValue(value);
130
+ await write((draft) => {
131
+ draft.set(key, text);
132
+ });
133
+ },
134
+ async update(key, change) {
135
+ checkKey(key);
136
+ return write((draft) => {
137
+ const text = draft.get(key);
138
+ const encoded = encodeValue(runChange(change, text === void 0 ? void 0 : decodeValue(text)));
139
+ draft.set(key, encoded);
140
+ return decodeValue(encoded);
141
+ });
142
+ },
143
+ async delete(key) {
144
+ checkKey(key);
145
+ await write((draft) => {
146
+ draft.delete(key);
147
+ });
148
+ },
149
+ async list(prefix = "") {
150
+ guard();
151
+ return [...state.keys()].filter((key) => key.startsWith(prefix)).sort(compareKeys);
152
+ }
153
+ },
154
+ close: async () => {
155
+ closed = true;
156
+ await tail;
157
+ }
158
+ };
159
+ }
160
+ //#endregion
161
+ export { openKv };
@@ -0,0 +1,39 @@
1
+ //#region ../../plugins/acryl-kv/lib/types/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
+ export { KvOpened as n, Kv as t };
package/lib/kv.d.ts ADDED
@@ -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 { type CopyKvOptions, type CopyKvResult, type JsonValue, KV_SERVICE, type Kv, type KvOpened, type NotPromise, checkKey, codePoints, compareKeys, copyKv, decodeValue, encodeValue, runChange };
package/lib/kv.js ADDED
@@ -0,0 +1,2 @@
1
+ import { a as compareKeys, c as runChange, i as codePoints, n as copyKv, o as decodeValue, r as checkKey, s as encodeValue, t as KV_SERVICE } from "./src-DRFVEQQb.js";
2
+ export { KV_SERVICE, checkKey, codePoints, compareKeys, copyKv, decodeValue, encodeValue, runChange };
@@ -0,0 +1,90 @@
1
+ //#region ../../plugins/acryl-kv/src/check.ts
2
+ /**
3
+ * 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.
4
+ */
5
+ /** A key is a non-empty string. */
6
+ function checkKey(key) {
7
+ if (typeof key !== "string" || key === "") throw new TypeError(`a kv key is a non-empty string, got ${typeof key === "string" ? "an empty string" : typeof key}`);
8
+ }
9
+ /**
10
+ * The JSON text of a value. `undefined`, functions, symbols, bigints, circular structures and non-finite numbers (which JSON would turn
11
+ * into `null`) are refused with a `TypeError`; nothing is stored for them.
12
+ */
13
+ function encodeValue(value) {
14
+ const text = JSON.stringify(value, (_key, part) => {
15
+ if (typeof part === "number" && !Number.isFinite(part)) throw new TypeError(`a kv value is JSON; ${String(part)} is not`);
16
+ return part;
17
+ });
18
+ if (text === void 0) throw new TypeError(`a kv value is JSON; ${typeof value} is not`);
19
+ return text;
20
+ }
21
+ /** The value of stored JSON text: a new copy each time, so changing what `get` returned changes nothing stored. */
22
+ function decodeValue(text) {
23
+ return JSON.parse(text);
24
+ }
25
+ /** Order two keys by code point (the order of UTF-8 bytes, which SQLite's binary collation and Postgres' "C" collation give too). */
26
+ function compareKeys(a, b) {
27
+ if (a === b) return 0;
28
+ const rank = (unit) => unit >= 55296 ? unit >= 57344 ? unit - 2048 : unit + 8192 : unit;
29
+ const length = Math.min(a.length, b.length);
30
+ for (let index = 0; index < length; index += 1) {
31
+ const difference = rank(a.charCodeAt(index)) - rank(b.charCodeAt(index));
32
+ if (difference !== 0) return difference;
33
+ }
34
+ return a.length - b.length;
35
+ }
36
+ /** The number of code points in a string (what SQL's `substr` counts). */
37
+ function codePoints(text) {
38
+ let count = 0;
39
+ for (const _ of text) count += 1;
40
+ return count;
41
+ }
42
+ /**
43
+ * Call an `update` callback and check what it returns: the callback is synchronous, so a promise (an `async` function the types should have
44
+ * refused) is rejected with a message that says what to do, instead of being stored as `{}`.
45
+ */
46
+ function runChange(change, current) {
47
+ const next = change(current);
48
+ if (typeof next === "object" && next !== null && typeof next.then === "function") throw new TypeError("the callback of kv.update is synchronous and returned a promise: read what you need first (await kv.get), then update with a plain function");
49
+ return next;
50
+ }
51
+ //#endregion
52
+ //#region ../../plugins/acryl-kv/src/copy.ts
53
+ /**
54
+ * Copy every key of one store into another: the import when an app moves from a JSON file to SQL (the file opened with
55
+ * `acryl-kv-json` is the source, `kvOnDb` the target). The source is not changed.
56
+ * @throws when the target has a key the source has and `overwrite` is not set; nothing is written then.
57
+ */
58
+ async function copyKv(source, target, options = {}) {
59
+ const keys = await source.list(options.prefix);
60
+ const existing = new Set(await target.list(options.prefix));
61
+ const shared = keys.filter((key) => existing.has(key));
62
+ if (shared.length > 0 && options.overwrite !== true) {
63
+ const shown = shared.slice(0, 5).map((key) => JSON.stringify(key)).join(", ");
64
+ throw new Error(`copyKv: the target already has ${shared.length} of these keys (${shown}${shared.length > 5 ? ", ..." : ""}); pass overwrite to replace them`);
65
+ }
66
+ let copied = 0;
67
+ for (const key of keys) {
68
+ const value = await source.get(key);
69
+ if (value === void 0) continue;
70
+ await target.set(key, value);
71
+ copied += 1;
72
+ }
73
+ return {
74
+ copied,
75
+ overwritten: shared.length
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region ../../plugins/acryl-kv/src/index.ts
80
+ /**
81
+ * The `kv` port: named JSON documents for an app too small for SQL, whichever provider serves it (`acryl-kv-json` for one file,
82
+ * `acryl-kv-db` over the `db` port). Plain code opens a store with a provider's `openKv`; a Cordis plugin declares `inject: ['kv']`, imports
83
+ * `acryl-kv/cordis` for the `ctx.kv` type, and never imports a provider.
84
+ *
85
+ * This entry imports nothing from Cordis, at runtime or in its types.
86
+ */
87
+ /** The service name every `kv` provider registers. */
88
+ const KV_SERVICE = "kv";
89
+ //#endregion
90
+ export { compareKeys as a, runChange as c, codePoints as i, copyKv as n, decodeValue as o, checkKey as r, encodeValue as s, KV_SERVICE as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acrylblends",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "The Acryl web framework (import { Acryl } from \"acrylblends\") and the acrylblends command that scaffolds Cordis apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -426,6 +426,18 @@
426
426
  "types": "./lib/db/sqlite.d.ts",
427
427
  "default": "./lib/db/sqlite.js"
428
428
  },
429
+ "./kv": {
430
+ "types": "./lib/kv.d.ts",
431
+ "default": "./lib/kv.js"
432
+ },
433
+ "./kv/json": {
434
+ "types": "./lib/kv/json.d.ts",
435
+ "default": "./lib/kv/json.js"
436
+ },
437
+ "./kv/db": {
438
+ "types": "./lib/kv/db.d.ts",
439
+ "default": "./lib/kv/db.js"
440
+ },
429
441
  "./package.json": "./package.json",
430
442
  "./cli": {
431
443
  "types": "./dist/index.d.ts",
@@ -2,7 +2,7 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "plugins/acryl-db",
5
- "commit": "8e23e14336647943d4232047e3ea253e434535d3"
5
+ "commit": "f8e385e4389fe2fdc6fd2a4366a7f2a7389faacb"
6
6
  },
7
7
  "vendored": [
8
8
  "src",
@@ -2,7 +2,7 @@
2
2
  "source": {
3
3
  "repo": "acryldev/acryl",
4
4
  "path": "plugins/acryl-db-sqlite",
5
- "commit": "8e23e14336647943d4232047e3ea253e434535d3"
5
+ "commit": "f8e385e4389fe2fdc6fd2a4366a7f2a7389faacb"
6
6
  },
7
7
  "vendored": [
8
8
  "src",