acrylblends 0.7.4 → 0.7.6
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.js +193 -1
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +2 -1
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/getting-started/deploy.md +5 -5
- package/docs/framework/getting-started/service-worker.md +5 -3
- package/docs/framework/guides/create-acrylblends.md +2 -2
- package/docs/framework/guides/data.md +237 -0
- package/docs/framework/guides/forms-and-sessions.md +34 -30
- package/docs/framework/upstream.json +3 -3
- package/lib/db/sqlite.d.ts +27 -0
- package/lib/db/sqlite.js +197 -0
- package/lib/db.d.ts +2 -0
- package/lib/db.js +2 -0
- package/lib/index-D4FAsBZu.d.ts +85 -0
- package/lib/src-MWcsMS8i.js +85 -0
- package/package.json +9 -1
- package/plugins/acryl-db/LICENSE +47 -0
- package/plugins/acryl-db/plugin.json +12 -0
- package/plugins/acryl-db/src/cordis.ts +13 -0
- package/plugins/acryl-db/src/facade/database.ts +41 -0
- package/plugins/acryl-db/src/facade/migrations.ts +50 -0
- package/plugins/acryl-db/src/facade/schema.ts +9 -0
- package/plugins/acryl-db/src/index.ts +19 -0
- package/plugins/acryl-db/src/migration-files.ts +31 -0
- package/plugins/acryl-db/upstream.json +18 -0
- package/plugins/acryl-db-sqlite/LICENSE +47 -0
- package/plugins/acryl-db-sqlite/plugin.json +13 -0
- package/plugins/acryl-db-sqlite/src/facade/node.ts +199 -0
- package/plugins/acryl-db-sqlite/src/index.ts +88 -0
- package/plugins/acryl-db-sqlite/src/open.ts +61 -0
- package/plugins/acryl-db-sqlite/upstream.json +21 -0
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/upstream.json +1 -1
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +13 -3
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/tsconfig.json +1 -0
- package/templates/todo/upstream.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { StatementSync } from "node:sqlite";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import type { Db, DbExecutor, SqlValue } from "../../../acryl-db/src/index.ts";
|
|
6
|
+
|
|
7
|
+
/** Node SQLite connection settings for a database file. */
|
|
8
|
+
export type NodeSqliteDatabaseOptions = {
|
|
9
|
+
/** SQLite WAL auto-checkpoint threshold. SQLite and this adapter default to 1,000 pages; 0 disables it. */
|
|
10
|
+
readonly walAutoCheckpointPages?: number;
|
|
11
|
+
/** Time SQLite waits for a competing file lock. SQLite defaults to 0; this adapter defaults to 5,000 ms. */
|
|
12
|
+
readonly busyTimeoutMs?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DEFAULT_WAL_AUTO_CHECKPOINT_PAGES = 1_000;
|
|
16
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5_000;
|
|
17
|
+
|
|
18
|
+
type TransactionScope = { active: boolean };
|
|
19
|
+
|
|
20
|
+
const ignore = (): void => {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runs operations in call order. An operation starts immediately when nothing is running or waiting;
|
|
24
|
+
* otherwise it waits for everything before it. An asynchronous operation holds the queue until it settles.
|
|
25
|
+
*/
|
|
26
|
+
class SerialOperationQueue {
|
|
27
|
+
private tail: Promise<void> = Promise.resolve();
|
|
28
|
+
private pending = 0;
|
|
29
|
+
|
|
30
|
+
run<T>(operation: () => T): Promise<T> {
|
|
31
|
+
if (this.pending > 0) return this.enqueue(operation);
|
|
32
|
+
try {
|
|
33
|
+
return Promise.resolve(operation());
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return Promise.reject(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
runAsync<T>(operation: () => Promise<T>): Promise<T> {
|
|
40
|
+
if (this.pending > 0) return this.enqueue(operation);
|
|
41
|
+
this.pending++;
|
|
42
|
+
// Publish the barrier before the operation starts, so calls it makes synchronously wait behind it.
|
|
43
|
+
const { promise: barrier, resolve: releaseBarrier } = Promise.withResolvers<void>();
|
|
44
|
+
this.tail = barrier;
|
|
45
|
+
let started: Promise<T>;
|
|
46
|
+
try {
|
|
47
|
+
started = operation();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
started = Promise.reject(error);
|
|
50
|
+
}
|
|
51
|
+
return started.finally(() => {
|
|
52
|
+
this.pending--;
|
|
53
|
+
releaseBarrier();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private enqueue<T>(operation: () => T | Promise<T>): Promise<T> {
|
|
58
|
+
this.pending++;
|
|
59
|
+
return this.release(this.tail.then(operation));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private release<T>(operation: Promise<T>): Promise<T> {
|
|
63
|
+
const settled = operation.finally(() => {
|
|
64
|
+
this.pending--;
|
|
65
|
+
});
|
|
66
|
+
this.tail = settled.then(ignore, ignore);
|
|
67
|
+
return settled;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Executes SQL on one connection. Prepared statements are cached per connection by SQL text, so the
|
|
73
|
+
* database and its transaction handles share them across transactions.
|
|
74
|
+
*/
|
|
75
|
+
abstract class NodeSqliteExecutor implements DbExecutor {
|
|
76
|
+
protected readonly database: DatabaseSync;
|
|
77
|
+
protected readonly statements: Map<string, StatementSync>;
|
|
78
|
+
|
|
79
|
+
constructor(database: DatabaseSync, statements: Map<string, StatementSync>) {
|
|
80
|
+
this.database = database;
|
|
81
|
+
this.statements = statements;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
exec(sql: string): Promise<void> {
|
|
85
|
+
return this.runOperation(() => {
|
|
86
|
+
this.database.exec(sql);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
run(sql: string, ...params: SqlValue[]): Promise<{ readonly changes: number }> {
|
|
91
|
+
return this.runOperation(() => ({ changes: Number(this.statement(sql).run(...params).changes) }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get<T extends object>(sql: string, ...params: SqlValue[]): Promise<T | undefined> {
|
|
95
|
+
return this.runOperation(() => this.statement(sql).get(...params) as T | undefined);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
all<T extends object>(sql: string, ...params: SqlValue[]): Promise<T[]> {
|
|
99
|
+
return this.runOperation(() => this.statement(sql).all(...params) as T[]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected abstract runOperation<T>(operation: () => T): Promise<T>;
|
|
103
|
+
|
|
104
|
+
private statement(sql: string): StatementSync {
|
|
105
|
+
let statement = this.statements.get(sql);
|
|
106
|
+
if (statement === undefined) {
|
|
107
|
+
statement = this.database.prepare(sql);
|
|
108
|
+
this.statements.set(sql, statement);
|
|
109
|
+
}
|
|
110
|
+
return statement;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class NodeSqliteTransaction extends NodeSqliteExecutor {
|
|
115
|
+
private readonly scope: TransactionScope;
|
|
116
|
+
|
|
117
|
+
constructor(database: DatabaseSync, statements: Map<string, StatementSync>, scope: TransactionScope) {
|
|
118
|
+
super(database, statements);
|
|
119
|
+
this.scope = scope;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected async runOperation<T>(operation: () => T): Promise<T> {
|
|
123
|
+
if (!this.scope.active) throw new Error("SQLite transaction handle is no longer active");
|
|
124
|
+
return operation();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `Db` adapter backed by Node's built-in `node:sqlite`. */
|
|
129
|
+
export class NodeSqliteDatabase extends NodeSqliteExecutor implements Db {
|
|
130
|
+
private readonly access = new SerialOperationQueue();
|
|
131
|
+
private closed = false;
|
|
132
|
+
|
|
133
|
+
constructor(database: DatabaseSync) {
|
|
134
|
+
super(database, new Map());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
transaction<T>(callback: (transaction: DbExecutor) => Promise<T>): Promise<T> {
|
|
138
|
+
return this.access.runAsync(async () => {
|
|
139
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
140
|
+
const scope = { active: true };
|
|
141
|
+
try {
|
|
142
|
+
const result = await callback(new NodeSqliteTransaction(this.database, this.statements, scope));
|
|
143
|
+
scope.active = false;
|
|
144
|
+
this.database.exec("COMMIT");
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
scope.active = false;
|
|
148
|
+
try {
|
|
149
|
+
this.database.exec("ROLLBACK");
|
|
150
|
+
} catch (rollbackError) {
|
|
151
|
+
throw new AggregateError([error, rollbackError], "SQLite transaction failed and rollback failed");
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
close(): Promise<void> {
|
|
159
|
+
return this.access.run(() => {
|
|
160
|
+
if (this.closed) return;
|
|
161
|
+
this.closed = true;
|
|
162
|
+
this.statements.clear();
|
|
163
|
+
try {
|
|
164
|
+
this.database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
165
|
+
} finally {
|
|
166
|
+
this.database.close();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected runOperation<T>(operation: () => T): Promise<T> {
|
|
172
|
+
return this.access.run(operation);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Open and configure a Node-backed SQLite database facade. */
|
|
177
|
+
export async function openNodeSqliteDatabase(
|
|
178
|
+
path: string,
|
|
179
|
+
options: NodeSqliteDatabaseOptions = {},
|
|
180
|
+
): Promise<NodeSqliteDatabase> {
|
|
181
|
+
const checkpointPages = options.walAutoCheckpointPages ?? DEFAULT_WAL_AUTO_CHECKPOINT_PAGES;
|
|
182
|
+
const timeout = options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS;
|
|
183
|
+
if (path !== ":memory:") await mkdir(dirname(path), { recursive: true });
|
|
184
|
+
const database = new DatabaseSync(path, { timeout });
|
|
185
|
+
const adapter = new NodeSqliteDatabase(database);
|
|
186
|
+
try {
|
|
187
|
+
await adapter.exec("PRAGMA journal_mode = WAL");
|
|
188
|
+
await adapter.exec("PRAGMA synchronous = NORMAL");
|
|
189
|
+
await adapter.exec(`PRAGMA wal_autocheckpoint = ${checkpointPages}`);
|
|
190
|
+
return adapter;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
try {
|
|
193
|
+
await adapter.close();
|
|
194
|
+
} catch {
|
|
195
|
+
// Preserve the configuration failure.
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQLite provider of the `db` port (spec 049 slice 1): one `node:sqlite` file, opened with the ported facade's settings (WAL,
|
|
3
|
+
* `synchronous = NORMAL`, a busy timeout), migrated, and provided as `db`. App plugins declare `inject: ['db']` and import types from
|
|
4
|
+
* `acryl-db` only, so replacing this row with another provider does not touch them. The plugin is a thin wrapper over `openDb`
|
|
5
|
+
* (`acryl-db-sqlite/open`), the same opening for code without Cordis.
|
|
6
|
+
*
|
|
7
|
+
* Unloading withdraws `db` first, so consumers stop calling, and then closes the file: the close waits for running operations and
|
|
8
|
+
* transactions, checkpoints the WAL and releases the file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
14
|
+
import z from '@deepseek-ai/schemastery'
|
|
15
|
+
import { DB_SERVICE } from '../../acryl-db/src/index.ts'
|
|
16
|
+
import type {} from '../../acryl-db/src/cordis.ts'
|
|
17
|
+
import { openDb } from './open.ts'
|
|
18
|
+
|
|
19
|
+
export { NodeSqliteDatabase, openNodeSqliteDatabase, type NodeSqliteDatabaseOptions } from './facade/node.ts'
|
|
20
|
+
export { openDb, type OpenDb, type OpenDbOptions } from './open.ts'
|
|
21
|
+
|
|
22
|
+
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
23
|
+
export const name = 'acryl-db-sqlite'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What this plugin reads of the runtime's optional `appInstance` service: its home folder. The service belongs to the runtime (and to an
|
|
27
|
+
* app's own instance plugin), so this plugin declares no type for it on Cordis' Context; it checks the one field it uses at runtime.
|
|
28
|
+
*/
|
|
29
|
+
interface AppHome {
|
|
30
|
+
readonly home: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isAppHome(value: unknown): value is AppHome {
|
|
34
|
+
return typeof value === 'object' && value !== null && 'home' in value && typeof value.home === 'string'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Config {
|
|
38
|
+
path?: string
|
|
39
|
+
migrations?: string
|
|
40
|
+
busyTimeoutMs?: number
|
|
41
|
+
walAutoCheckpointPages?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Config: z<Config> = z.object({
|
|
45
|
+
path: z.string().description('The database file, absolute or relative to the app. Defaults to data.sqlite in the ACRYL app home; required where there is none.'),
|
|
46
|
+
migrations: z.string().description('A folder of NNN-name.sql files numbered from 1, absolute or relative to the app, applied in order on start.'),
|
|
47
|
+
busyTimeoutMs: z.natural().description('How long SQLite waits for a competing file lock, in milliseconds. Defaults to 5000.'),
|
|
48
|
+
walAutoCheckpointPages: z.natural().description('The WAL auto-checkpoint threshold in pages; 0 disables it. Defaults to 1000.'),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** The file name used in the app home when `path` is not configured. */
|
|
52
|
+
export const DEFAULT_FILE = 'data.sqlite'
|
|
53
|
+
|
|
54
|
+
/** A configured path as a file path: absolute as given, relative to the Loader's base URL (the app) otherwise. */
|
|
55
|
+
function resolveFromApp(ctx: Context, path: string, option: string): string {
|
|
56
|
+
if (path.startsWith('file:')) return fileURLToPath(path)
|
|
57
|
+
if (isAbsolute(path)) return path
|
|
58
|
+
const base = ctx.baseUrl
|
|
59
|
+
if (base === undefined || !base.startsWith('file:')) {
|
|
60
|
+
throw new Error(`acryl-db-sqlite: ${option} ${JSON.stringify(path)} is relative, but this app has no file base URL to resolve it from; give an absolute path`)
|
|
61
|
+
}
|
|
62
|
+
const appDir = base.endsWith('/') ? fileURLToPath(base) : dirname(fileURLToPath(base))
|
|
63
|
+
return resolve(appDir, path)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function apply(ctx: Context, rawConfig: Config = {}): Promise<void> {
|
|
67
|
+
const config = Config(rawConfig)
|
|
68
|
+
// The app home comes from the runtime's `appInstance` service when this runs inside an ACRYL app; a plain framework app names its file.
|
|
69
|
+
const appInstance: unknown = ctx.get('appInstance')
|
|
70
|
+
let file: string
|
|
71
|
+
if (config.path !== undefined) file = resolveFromApp(ctx, config.path, 'path')
|
|
72
|
+
else if (isAppHome(appInstance)) file = join(appInstance.home, DEFAULT_FILE)
|
|
73
|
+
else throw new Error('acryl-db-sqlite: no database path; set `path` in this row\'s config (there is no ACRYL app home to default to)')
|
|
74
|
+
const migrations = config.migrations === undefined ? undefined : resolveFromApp(ctx, config.migrations, 'migrations')
|
|
75
|
+
const options = {
|
|
76
|
+
path: file,
|
|
77
|
+
...(migrations === undefined ? {} : { migrations }),
|
|
78
|
+
...(config.busyTimeoutMs === undefined ? {} : { busyTimeoutMs: config.busyTimeoutMs }),
|
|
79
|
+
...(config.walAutoCheckpointPages === undefined ? {} : { walAutoCheckpointPages: config.walAutoCheckpointPages }),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Disposers run in reverse: the service is withdrawn before the file closes.
|
|
83
|
+
await ctx.effect(async function* () {
|
|
84
|
+
const opened = await openDb(options)
|
|
85
|
+
yield () => opened.close()
|
|
86
|
+
yield ctx.reflect.provide(DB_SERVICE, opened.db)
|
|
87
|
+
}, `acryl-db-sqlite: ${file}`)
|
|
88
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plain way to open the SQLite provider, for code without Cordis (an acrylblends app): `openDb({ path, migrations })` opens one
|
|
3
|
+
* `node:sqlite` file with the ported facade's settings (WAL, `synchronous = NORMAL`, a busy timeout) and foreign keys enforced, applies the
|
|
4
|
+
* numbered migrations, and resolves the `db` port with the function that closes it. The Cordis plugin is a thin wrapper over this, so both
|
|
5
|
+
* run one code path.
|
|
6
|
+
* This entry imports nothing from Cordis.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { applyMigrations, MIGRATIONS_TABLE, readMigrationFiles, type Db, type SqlValue } from '../../acryl-db/src/index.ts'
|
|
10
|
+
import { openNodeSqliteDatabase } from './facade/node.ts'
|
|
11
|
+
|
|
12
|
+
export type { Db, DbExecutor, SqlValue } from '../../acryl-db/src/index.ts'
|
|
13
|
+
|
|
14
|
+
export interface OpenDbOptions {
|
|
15
|
+
/** The database file, its folder created if missing; `:memory:` for a database that lives until `close`. */
|
|
16
|
+
readonly path: string
|
|
17
|
+
/** A folder of `NNN-name.sql` files numbered from 1, as a path or a file URL, applied in order before `openDb` resolves. */
|
|
18
|
+
readonly migrations?: string | URL
|
|
19
|
+
/** How long SQLite waits for a competing file lock, in milliseconds. Defaults to 5000. */
|
|
20
|
+
readonly busyTimeoutMs?: number
|
|
21
|
+
/** The WAL auto-checkpoint threshold in pages; 0 disables it. Defaults to 1000. */
|
|
22
|
+
readonly walAutoCheckpointPages?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An open database: the port, and the one function that closes it. */
|
|
26
|
+
export interface OpenDb {
|
|
27
|
+
readonly db: Db
|
|
28
|
+
/** Waits for running operations and transactions, checkpoints the WAL and releases the file. Calling it again does nothing. */
|
|
29
|
+
close(): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Open, configure and migrate a SQLite database.
|
|
34
|
+
* @throws when the migration files are misnamed or misnumbered, or a migration fails (the file is closed, and unchanged), or the
|
|
35
|
+
* database's schema is newer than the migrations.
|
|
36
|
+
*/
|
|
37
|
+
export async function openDb(options: OpenDbOptions): Promise<OpenDb> {
|
|
38
|
+
const migrations = options.migrations === undefined ? [] : await readMigrationFiles(options.migrations)
|
|
39
|
+
const database = await openNodeSqliteDatabase(options.path, {
|
|
40
|
+
...(options.busyTimeoutMs === undefined ? {} : { busyTimeoutMs: options.busyTimeoutMs }),
|
|
41
|
+
...(options.walAutoCheckpointPages === undefined ? {} : { walAutoCheckpointPages: options.walAutoCheckpointPages }),
|
|
42
|
+
})
|
|
43
|
+
try {
|
|
44
|
+
// Off by default in SQLite, per connection, and ignored inside a transaction: set here, before the migrations' transaction. The port
|
|
45
|
+
// promises it (`ON DELETE CASCADE` works on every provider).
|
|
46
|
+
await database.exec('PRAGMA foreign_keys = ON')
|
|
47
|
+
await applyMigrations(database, migrations, { table: MIGRATIONS_TABLE })
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await database.close()
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
// The port, not the adapter: the code that receives `db` gets no `close`, which belongs to whoever opened it.
|
|
53
|
+
const db: Db = {
|
|
54
|
+
exec: sql => database.exec(sql),
|
|
55
|
+
run: (sql, ...params) => database.run(sql, ...params),
|
|
56
|
+
get: <T extends object>(sql: string, ...params: SqlValue[]) => database.get<T>(sql, ...params),
|
|
57
|
+
all: <T extends object>(sql: string, ...params: SqlValue[]) => database.all<T>(sql, ...params),
|
|
58
|
+
transaction: work => database.transaction(work),
|
|
59
|
+
}
|
|
60
|
+
return { db, close: () => database.close() }
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"repo": "acryldev/acryl",
|
|
4
|
+
"path": "plugins/acryl-db-sqlite",
|
|
5
|
+
"commit": "8e23e14336647943d4232047e3ea253e434535d3"
|
|
6
|
+
},
|
|
7
|
+
"vendored": [
|
|
8
|
+
"src",
|
|
9
|
+
"tests",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"rewrittenSpecifiers": 9,
|
|
13
|
+
"digest": "sha256:853af986073e5721c9b8fb5626b46bcd1ca8a6c5f936c4ac11fc3c21f3c5c0ef",
|
|
14
|
+
"testsNotRun": {
|
|
15
|
+
"tests/loader.spec.ts": "loads the plugin through the real Loader (@deepseek-ai/cordis-plugin-loader), which generated apps do not install; the plain opener it wraps is tested here by open.spec.ts and conformance.spec.ts",
|
|
16
|
+
"tests/plain-entry-imports.spec.ts": "a copy vendored into an app (with relative imports, no build of its own) has neither `lib/`; the app's own typecheck is what proves the copy names no Cordis type it does not provide."
|
|
17
|
+
},
|
|
18
|
+
"notice": [
|
|
19
|
+
"LICENSE"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"apps/acryl-desktop/src/shell/desktop-user-data.ts": "user-data.ts",
|
|
6
6
|
"apps/acryl-desktop/src/shell/shutdown.ts": "shutdown.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "8e23e14336647943d4232047e3ea253e434535d3"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 0,
|
|
11
11
|
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The desktop shell of a plain Acryl app: `npm run desktop` serves the app (`src/app.ts
|
|
2
|
+
* The desktop shell of a plain Acryl app: `npm run desktop` serves the app (`src/app.ts`: `export default app`, and optionally `export const close` for a database to close) on a free loopback port from this Electron main process and
|
|
3
3
|
* opens one window on it. Nothing in the app imports electron; every Electron call is in this file and `window.ts`, so a different shell is a change here only.
|
|
4
4
|
*
|
|
5
5
|
* Order matters: the user-data folder is set before anything else (and before the single-instance lock), so one app never shares window state, logs or the lock with
|
|
@@ -36,13 +36,19 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
36
36
|
} else {
|
|
37
37
|
let server: ReturnType<typeof serve> | undefined
|
|
38
38
|
let window: BrowserWindow | undefined
|
|
39
|
+
/** The app module's optional `close`, for an app that owns a database: called once the server has stopped. */
|
|
40
|
+
let closeApp: (() => Promise<void>) | undefined
|
|
39
41
|
|
|
40
42
|
const shutdown = createDesktopShutdown(
|
|
41
43
|
() =>
|
|
42
44
|
new Promise<void>((done) => {
|
|
43
45
|
if (server === undefined) return done()
|
|
44
46
|
server.close(() => {
|
|
45
|
-
|
|
47
|
+
const closing = closeApp?.() ?? Promise.resolve()
|
|
48
|
+
closing.then(
|
|
49
|
+
() => done(),
|
|
50
|
+
() => done(),
|
|
51
|
+
)
|
|
46
52
|
})
|
|
47
53
|
// Open streams and keep-alive connections would hold close() open: end them.
|
|
48
54
|
;(server as { closeAllConnections?: () => void }).closeAllConnections?.()
|
|
@@ -67,7 +73,11 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
67
73
|
|
|
68
74
|
void app.whenReady().then(async () => {
|
|
69
75
|
// The hook above is registered before the app's modules are loaded, so the app may have .tsx pages: they are loaded here, not as a static import.
|
|
70
|
-
const { default: acrylApp } = await import('../../app.ts')
|
|
76
|
+
const { default: acrylApp, close } = (await import('../../app.ts')) as {
|
|
77
|
+
default: { fetch: Parameters<typeof serve>[0]['fetch'] }
|
|
78
|
+
close?: () => Promise<void>
|
|
79
|
+
}
|
|
80
|
+
closeApp = close
|
|
71
81
|
const port = await new Promise<number>((resolve) => {
|
|
72
82
|
server = serve(
|
|
73
83
|
{ fetch: acrylApp.fetch, port: 0, hostname: '127.0.0.1' },
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"apps/acryl-desktop/src/shell/desktop-user-data.ts": "user-data.ts",
|
|
6
6
|
"apps/acryl-desktop/src/shell/shutdown.ts": "shutdown.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "8e23e14336647943d4232047e3ea253e434535d3"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 0,
|
|
11
11
|
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|
package/plugins/tsconfig.json
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"acryl-*/src/**/*.case.ts",
|
|
24
24
|
"acryl-node-server/tests/adapter",
|
|
25
25
|
"acryl-router/tests/loader.spec.ts",
|
|
26
|
+
"acryl-db-sqlite/tests/loader.spec.ts",
|
|
26
27
|
"acryl-state/src/react.ts",
|
|
27
28
|
"acryl-state/tests/react.spec.ts",
|
|
28
29
|
"acryl-router-http/tests/adapter.spec.ts",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"examples/acryl-todo/src/index.ts": "src/todo.ts",
|
|
6
6
|
"examples/acryl-todo/src/page.ts": "src/page.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "8e23e14336647943d4232047e3ea253e434535d3"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 4,
|
|
11
11
|
"digest": "sha256:9093704776563001f7f472f94cc5c2fdd79206342c1cb59c11fce62aa74df3ed"
|