@toist/aja 0.5.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.
- package/CHANGELOG.md +69 -0
- package/migrations/001_initial.sql +111 -0
- package/package.json +27 -0
- package/src/cache-db.ts +17 -0
- package/src/cache.ts +67 -0
- package/src/config.ts +129 -0
- package/src/data-db.ts +21 -0
- package/src/db-handles.ts +70 -0
- package/src/hitl.ts +257 -0
- package/src/index.ts +34 -0
- package/src/instance.ts +64 -0
- package/src/kinds/control.ts +26 -0
- package/src/kinds/custom.ts +19 -0
- package/src/kinds/data.ts +30 -0
- package/src/kinds/db.ts +92 -0
- package/src/kinds/hitl.ts +56 -0
- package/src/kinds/http.ts +134 -0
- package/src/kinds/index.ts +66 -0
- package/src/kinds/runs.ts +130 -0
- package/src/kinds/transform.ts +123 -0
- package/src/kinds/types.ts +16 -0
- package/src/lock.ts +64 -0
- package/src/migrate.ts +204 -0
- package/src/pipeline.ts +601 -0
- package/src/resources.ts +350 -0
- package/src/runs.ts +53 -0
- package/src/runtime-db.ts +48 -0
- package/src/server.ts +537 -0
- package/src/startRunner.ts +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@toist/aja` are recorded here.
|
|
4
|
+
|
|
5
|
+
## 0.5.0 — 2026-05-05
|
|
6
|
+
|
|
7
|
+
Lockstep version bump. No functional changes in this package.
|
|
8
|
+
|
|
9
|
+
## 0.4.0 — 2026-05-05
|
|
10
|
+
|
|
11
|
+
**Breaking rename:** package was `@toist/run`; it is now `@toist/aja`.
|
|
12
|
+
Update your `package.json` dependency and any `import ... from "@toist/run"`
|
|
13
|
+
statements to `"@toist/aja"`. No API or behaviour changes.
|
|
14
|
+
|
|
15
|
+
## 0.3.0 — 2026-05-05
|
|
16
|
+
|
|
17
|
+
Lockstep version bump alongside the new `@toist/up` package. No
|
|
18
|
+
functional changes in this package.
|
|
19
|
+
|
|
20
|
+
## 0.2.2 — 2026-05-05
|
|
21
|
+
|
|
22
|
+
**Fix:** UI assets now ship with explicit `Content-Type` headers. The
|
|
23
|
+
previous version relied on `new Response(Bun.file(path))` to carry the
|
|
24
|
+
MIME type through automatically, which works in some hosts but not
|
|
25
|
+
others — observed in published installs (consumer's `node_modules/@toist/ui`)
|
|
26
|
+
the header arrived empty, tripping the browser's strict MIME check on
|
|
27
|
+
JS module scripts (`Failed to load module script: MIME type ""`). The
|
|
28
|
+
runner now sets `Content-Type` from `BunFile.type` explicitly. SPA
|
|
29
|
+
fallback uses the same path.
|
|
30
|
+
|
|
31
|
+
## 0.2.1 — 2026-05-05
|
|
32
|
+
|
|
33
|
+
Lockstep version bump alongside `@toist/in@0.2.1`. No functional changes
|
|
34
|
+
in this package.
|
|
35
|
+
|
|
36
|
+
## 0.2.0 — 2026-05-05
|
|
37
|
+
|
|
38
|
+
Rebrand: `@2121/runner` → `@toist/aja`. Republished to **npmjs.com**
|
|
39
|
+
(public scope). See `@toist/spec` CHANGELOG for the full rationale.
|
|
40
|
+
|
|
41
|
+
No API or behaviour changes from 0.1.0; the rename is the only breaking
|
|
42
|
+
change. Workspace deps now reference `@toist/spec` and `@toist/ui`.
|
|
43
|
+
|
|
44
|
+
## 0.1.0 — 2026-05-05 — abandoned (@2121/runner on Gitea)
|
|
45
|
+
|
|
46
|
+
Initial release. The pipeline runtime, extracted from the platform
|
|
47
|
+
monorepo per Phase F W2 and shipped to the Gitea npm registry per W3.
|
|
48
|
+
|
|
49
|
+
- Public API: `startRunner(options): Promise<RunnerHandle>`,
|
|
50
|
+
`register(...kinds)`, `getKind(id)`, `manifest()`. See instance-spec at
|
|
51
|
+
https://toist.in for the full contract.
|
|
52
|
+
- HTTP server (Hono on Bun) — mounts API at `/api/*` and serves the
|
|
53
|
+
bundled UI from `@toist/ui`'s `distDir` at `/`.
|
|
54
|
+
- Pipeline dispatcher with HITL-aware suspendable executor (per-node
|
|
55
|
+
memoization for resume-skip; tasks queue with single-use response
|
|
56
|
+
tokens).
|
|
57
|
+
- Kind registry with built-in kinds: `if`, `for`, `try`, `parallel`,
|
|
58
|
+
`human.input`, `js.eval`, `http.request`, `pipeline.invoke`,
|
|
59
|
+
`chat.completion`, and platform-internal helpers. Custom kinds register
|
|
60
|
+
via `register(...)` before `startRunner`.
|
|
61
|
+
- Resource resolution per resource-spec three-tier: ENV > DB > YAML.
|
|
62
|
+
- Persistence: SQLite — `runtime.db` (run ledger, logs, HITL tasks,
|
|
63
|
+
resources), `cache.db` (TTL'd cache), `data.db` (host-owned domain
|
|
64
|
+
state). All under `dataDir`.
|
|
65
|
+
- Migration runner: linear, checksum-tracked SQL files in `migrations/`.
|
|
66
|
+
This release ships a single squashed `001_initial.sql` baseline.
|
|
67
|
+
- StartRunner options: `disableUi`, `disableMcp`, `disableWatch`,
|
|
68
|
+
`corsOrigins`. MCP server surface is reserved (instance-spec §12);
|
|
69
|
+
not yet served from this process.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
-- 2121 platform migration 001: initial runtime schema (v0.1 baseline).
|
|
2
|
+
--
|
|
3
|
+
-- Squashed from the pre-publish migrations (initial + HITL + resources). For
|
|
4
|
+
-- the first published version of @2121/runner this is the single baseline a
|
|
5
|
+
-- fresh instance applies on first start. Future migrations stack on top.
|
|
6
|
+
--
|
|
7
|
+
-- All statements use IF NOT EXISTS so a surgical re-baseline (DELETE FROM
|
|
8
|
+
-- _migrations on a populated DB) is a safe no-op against existing schema.
|
|
9
|
+
--
|
|
10
|
+
-- Cache lives in its own cache.db (see cache.ts) and is not migration-managed
|
|
11
|
+
-- — its TTL semantics make drop-recreate safe. Host domain data lives in
|
|
12
|
+
-- data.db, schema-owned by the host repo.
|
|
13
|
+
|
|
14
|
+
-- ---------------------------------------------------------------------------
|
|
15
|
+
-- runs — the run ledger. Every pipeline invocation gets a row here, kept for
|
|
16
|
+
-- the lifetime of the runtime DB. current_node + updated_at + trigger support
|
|
17
|
+
-- the suspendable executor (HITL pause/resume).
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
pipeline TEXT NOT NULL,
|
|
22
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
23
|
+
payload TEXT,
|
|
24
|
+
result TEXT,
|
|
25
|
+
steps TEXT,
|
|
26
|
+
error TEXT,
|
|
27
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
28
|
+
finished_at TEXT,
|
|
29
|
+
current_node TEXT,
|
|
30
|
+
updated_at TEXT,
|
|
31
|
+
trigger TEXT NOT NULL DEFAULT 'api'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_runs_pipeline ON runs(pipeline);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_runs_started ON runs(started_at DESC);
|
|
36
|
+
|
|
37
|
+
-- ---------------------------------------------------------------------------
|
|
38
|
+
-- logs — append-only per-run log lines. Cascades on run deletion.
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
run_id INTEGER REFERENCES runs(id) ON DELETE CASCADE,
|
|
43
|
+
ts TEXT DEFAULT (datetime('now')),
|
|
44
|
+
level TEXT DEFAULT 'info',
|
|
45
|
+
msg TEXT
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_logs_run ON logs(run_id);
|
|
49
|
+
|
|
50
|
+
-- ---------------------------------------------------------------------------
|
|
51
|
+
-- node_outputs — per-node memoization for resume-skip. On resume, the
|
|
52
|
+
-- executor hydrates results from this table and skips any node that already
|
|
53
|
+
-- has an output recorded; only the suspended node and its successors actually
|
|
54
|
+
-- re-execute.
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS node_outputs (
|
|
57
|
+
run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
58
|
+
node_id TEXT NOT NULL,
|
|
59
|
+
output_json TEXT,
|
|
60
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
61
|
+
finished_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
62
|
+
PRIMARY KEY (run_id, node_id)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_node_outputs_run ON node_outputs(run_id);
|
|
66
|
+
|
|
67
|
+
-- ---------------------------------------------------------------------------
|
|
68
|
+
-- tasks — HITL pending-input queue. One row per human.input occurrence in a
|
|
69
|
+
-- run. response_token is single-use (UNIQUE), giving idempotency for retried
|
|
70
|
+
-- resume requests for free. assignee is polymorphic: 'user:<email>' or
|
|
71
|
+
-- 'agent:<id>'.
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
run_id INTEGER NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
76
|
+
node_id TEXT NOT NULL,
|
|
77
|
+
kind TEXT NOT NULL, -- e.g. 'human.input'
|
|
78
|
+
prompt TEXT,
|
|
79
|
+
schema_json TEXT,
|
|
80
|
+
assignee TEXT,
|
|
81
|
+
status TEXT NOT NULL DEFAULT 'open', -- open|answered|expired|cancelled
|
|
82
|
+
response_json TEXT,
|
|
83
|
+
response_token TEXT NOT NULL UNIQUE,
|
|
84
|
+
responded_by TEXT,
|
|
85
|
+
responded_at TEXT,
|
|
86
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
87
|
+
expires_at TEXT
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_run ON tasks(run_id);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee);
|
|
93
|
+
|
|
94
|
+
-- ---------------------------------------------------------------------------
|
|
95
|
+
-- resources — UI/API-managed resource instances. YAML-file and ENV-VAR
|
|
96
|
+
-- resources are not stored here; this table holds only the "UI/API mutation"
|
|
97
|
+
-- tier of the three-tier override chain. Priority: ENV > DB (this table) >
|
|
98
|
+
-- YAML files. Sensitive fields are stored clear-text in v1 with a load-time
|
|
99
|
+
-- warning; encryption at rest is deferred (pipeline-spec.md §16.1).
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS resources (
|
|
102
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
103
|
+
name TEXT NOT NULL UNIQUE, -- access key in ctx.resource.<name>
|
|
104
|
+
type TEXT NOT NULL, -- ResourceType name (e.g. "AnthropicApi")
|
|
105
|
+
fields_json TEXT NOT NULL DEFAULT '{}', -- JSON object of field values
|
|
106
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
107
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_resources_name ON resources(name);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_resources_type ON resources(type);
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toist/aja",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"migrations/",
|
|
13
|
+
"CHANGELOG.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "bun --watch src/server.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@toist/spec": "0.5.0",
|
|
20
|
+
"@toist/ui": "0.5.0",
|
|
21
|
+
"hono": "^4.7.7",
|
|
22
|
+
"proper-lockfile": "^4.1.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/proper-lockfile": "^4.1.4"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/cache-db.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Cache database — separate file from runtime.db so its drop-recreate
|
|
3
|
+
// semantics (TTL makes content valueless) do not entangle runtime durability.
|
|
4
|
+
//
|
|
5
|
+
// Schema is owned and bootstrapped by makeCache() in cache.ts. No migrations
|
|
6
|
+
// here: cache table can be dropped or recreated freely.
|
|
7
|
+
|
|
8
|
+
import { Database } from "bun:sqlite"
|
|
9
|
+
import { mkdirSync } from "node:fs"
|
|
10
|
+
import { dirname } from "node:path"
|
|
11
|
+
import { cacheDbPath } from "./config.ts"
|
|
12
|
+
|
|
13
|
+
export function openCacheDb(): Database {
|
|
14
|
+
const path = cacheDbPath()
|
|
15
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
16
|
+
return new Database(path, { create: true })
|
|
17
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Sqlite-backed cache with TTL. Kindit memoisoivat kalliit fetchit transparentisti
|
|
3
|
+
// pipelinelle: ctx.cache.get/set, key on canonical hash inputeista + paramsista.
|
|
4
|
+
//
|
|
5
|
+
// TTL accepts: "30s" | "5m" | "2h" | "1d" or seconds as number. Default: "1h".
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite"
|
|
8
|
+
import { createHash } from "node:crypto"
|
|
9
|
+
import type { Cache } from "@toist/spec"
|
|
10
|
+
|
|
11
|
+
export type { Cache }
|
|
12
|
+
|
|
13
|
+
const TTL_RE = /^(\d+)(s|m|h|d)$/
|
|
14
|
+
const DEFAULT_TTL_SECONDS = 3600
|
|
15
|
+
|
|
16
|
+
function ttlToSeconds(ttl: string | number | undefined): number {
|
|
17
|
+
if (ttl === undefined) return DEFAULT_TTL_SECONDS
|
|
18
|
+
if (typeof ttl === "number") return ttl
|
|
19
|
+
const m = TTL_RE.exec(ttl)
|
|
20
|
+
if (!m) throw new Error(`Invalid TTL: ${ttl}`)
|
|
21
|
+
const n = Number(m[1])
|
|
22
|
+
const unit = m[2]
|
|
23
|
+
return unit === "s" ? n : unit === "m" ? n * 60 : unit === "h" ? n * 3600 : n * 86400
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function makeCache(db: Database): Cache {
|
|
27
|
+
db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
29
|
+
key TEXT PRIMARY KEY,
|
|
30
|
+
value TEXT NOT NULL,
|
|
31
|
+
expires_at INTEGER NOT NULL
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
|
|
34
|
+
`)
|
|
35
|
+
|
|
36
|
+
const get = db.prepare("SELECT value, expires_at FROM cache WHERE key = ?")
|
|
37
|
+
const set = db.prepare("INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)")
|
|
38
|
+
const del = db.prepare("DELETE FROM cache WHERE key = ?")
|
|
39
|
+
const prune = db.prepare("DELETE FROM cache WHERE expires_at < ?")
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
key(...parts) {
|
|
43
|
+
const canon = JSON.stringify(parts)
|
|
44
|
+
return createHash("sha1").update(canon).digest("hex")
|
|
45
|
+
},
|
|
46
|
+
get<T>(key: string): T | undefined {
|
|
47
|
+
const row = get.get(key) as { value: string; expires_at: number } | undefined
|
|
48
|
+
if (!row) return undefined
|
|
49
|
+
if (row.expires_at < Date.now()) {
|
|
50
|
+
del.run(key)
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
try { return JSON.parse(row.value) as T } catch { return undefined }
|
|
54
|
+
},
|
|
55
|
+
set(key, value, opts) {
|
|
56
|
+
const expires = Date.now() + ttlToSeconds(opts?.ttl) * 1000
|
|
57
|
+
set.run(key, JSON.stringify(value), expires)
|
|
58
|
+
},
|
|
59
|
+
delete(key) {
|
|
60
|
+
del.run(key)
|
|
61
|
+
},
|
|
62
|
+
prune() {
|
|
63
|
+
const r = prune.run(Date.now())
|
|
64
|
+
return Number(r.changes ?? 0)
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Single source of truth for instance-relative paths. Per
|
|
3
|
+
// context/instance-spec.md §3 + §5:
|
|
4
|
+
//
|
|
5
|
+
// <rootDir>/
|
|
6
|
+
// instance.json — optional metadata (§6)
|
|
7
|
+
// pipelines/ — pipeline YAML files
|
|
8
|
+
// resources/ — resource YAML files
|
|
9
|
+
// data/ — runner-managed runtime state (gitignored):
|
|
10
|
+
// data.db — host's domain data (schema-on-write)
|
|
11
|
+
// runtime.db — platform ledger (runs/logs/tasks/node_outputs)
|
|
12
|
+
// cache.db — TTL-driven cache
|
|
13
|
+
// .lock — proper-lockfile holder (single-leader guard)
|
|
14
|
+
//
|
|
15
|
+
// rootDir resolution order:
|
|
16
|
+
// 1. process.env.PLATFORM_ROOT_DIR — explicit override (used by hosts
|
|
17
|
+
// that bootstrap @toist/aja from
|
|
18
|
+
// a non-cwd-aligned location)
|
|
19
|
+
// 2. join(process.cwd(), "..") — fallback matching the historic
|
|
20
|
+
// "server cwd is <root>/server"
|
|
21
|
+
// convention
|
|
22
|
+
//
|
|
23
|
+
// Note: the legacy `<rootDir>/.platform/` directory is platform-internal
|
|
24
|
+
// dev-mode coordination (pm2 port handoff in runtime.json, CLI tooling output
|
|
25
|
+
// in generated/). It is NOT part of the instance contract and is not exposed
|
|
26
|
+
// from this module — start.ts and ecosystem.config.cjs handle it on their own.
|
|
27
|
+
|
|
28
|
+
import { join } from "node:path"
|
|
29
|
+
|
|
30
|
+
let rootDirOverride: string | null = null
|
|
31
|
+
let uiDisabled = false
|
|
32
|
+
let watchDisabled = false
|
|
33
|
+
let corsOriginsConfig: string | string[] | null = null
|
|
34
|
+
|
|
35
|
+
/** Set the rootDir explicitly. Used by `startRunner({rootDir, ...})` to
|
|
36
|
+
* configure paths before the db modules' static imports trigger their
|
|
37
|
+
* module-load init. Once set, the override wins over PLATFORM_ROOT_DIR
|
|
38
|
+
* and the cwd-parent fallback. Calling twice is an error — rootDir is
|
|
39
|
+
* once-set per process. */
|
|
40
|
+
export function setRootDir(dir: string): void {
|
|
41
|
+
if (rootDirOverride !== null && rootDirOverride !== dir) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`[config] setRootDir(${JSON.stringify(dir)}) called after rootDir ` +
|
|
44
|
+
`was already set to ${JSON.stringify(rootDirOverride)}. ` +
|
|
45
|
+
`rootDir is once-set per process.`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
rootDirOverride = dir
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Set the disableUi flag. Used by `startRunner({disableUi: true, ...})` to
|
|
52
|
+
* opt out of the bundled static-UI mount before server.ts's outer app
|
|
53
|
+
* construction reads the flag. Default: UI is enabled when the dist/
|
|
54
|
+
* directory in @toist/ui exists. */
|
|
55
|
+
export function setDisableUi(v: boolean): void {
|
|
56
|
+
uiDisabled = v
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isUiDisabled(): boolean {
|
|
60
|
+
return uiDisabled
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Set the disableWatch flag. Used by `startRunner({disableWatch: true, ...})`
|
|
64
|
+
* to opt out of the filesystem watcher on pipelineDir/resourceDir. Default:
|
|
65
|
+
* watcher is enabled. Production deployments may set true to avoid
|
|
66
|
+
* inotify/fsevent overhead and accidental hot-reloads. */
|
|
67
|
+
export function setDisableWatch(v: boolean): void {
|
|
68
|
+
watchDisabled = v
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isWatchDisabled(): boolean {
|
|
72
|
+
return watchDisabled
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Set the CORS allow-origin configuration. `string` for a single origin,
|
|
76
|
+
* `string[]` for an allowlist, `null` for default (permissive `*`).
|
|
77
|
+
* Per Hono's cors() middleware semantics. */
|
|
78
|
+
export function setCorsOrigins(origins: string | string[] | null | undefined): void {
|
|
79
|
+
corsOriginsConfig = origins ?? null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getCorsOrigins(): string | string[] | null {
|
|
83
|
+
return corsOriginsConfig
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveRootDir(): string {
|
|
87
|
+
if (rootDirOverride !== null) return rootDirOverride
|
|
88
|
+
const override = process.env.PLATFORM_ROOT_DIR
|
|
89
|
+
if (override && override.length > 0) return override
|
|
90
|
+
return join(process.cwd(), "..")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function dataDir(): string {
|
|
94
|
+
const override = process.env.PLATFORM_DATA_DIR
|
|
95
|
+
if (override && override.length > 0) return override
|
|
96
|
+
return join(resolveRootDir(), "data")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function dataDbPath(): string {
|
|
100
|
+
return join(dataDir(), "data.db")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function runtimeDbPath(): string {
|
|
104
|
+
return join(dataDir(), "runtime.db")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function cacheDbPath(): string {
|
|
108
|
+
return join(dataDir(), "cache.db")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function lockfilePath(): string {
|
|
112
|
+
return join(dataDir(), ".lock")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function instanceFilePath(): string {
|
|
116
|
+
return join(resolveRootDir(), "instance.json")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function pipelinesDir(): string {
|
|
120
|
+
const override = process.env.PIPELINES_DIR
|
|
121
|
+
if (override && override.length > 0) return override
|
|
122
|
+
return join(resolveRootDir(), "pipelines")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resourcesDir(): string {
|
|
126
|
+
const override = process.env.RESOURCES_DIR
|
|
127
|
+
if (override && override.length > 0) return override
|
|
128
|
+
return join(resolveRootDir(), "resources")
|
|
129
|
+
}
|
package/src/data-db.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Domain data store. The instance owns the schema entirely — every table here
|
|
3
|
+
// is created by a pipeline (via db.insert) or by a domain migration the project
|
|
4
|
+
// chooses to add. Platform never writes to this DB directly.
|
|
5
|
+
//
|
|
6
|
+
// Kinds receive this as `ctx.db`. Pipelines write to it (db.insert), read
|
|
7
|
+
// from it (db.query), or any custom domain kind interacts with it as needed.
|
|
8
|
+
//
|
|
9
|
+
// Location: <dataDir>/data.db (per context/instance-spec.md §3 — the host's
|
|
10
|
+
// schema-on-write domain DB lives in data/ alongside runtime.db and cache.db).
|
|
11
|
+
|
|
12
|
+
import { Database } from "bun:sqlite"
|
|
13
|
+
import { mkdirSync } from "node:fs"
|
|
14
|
+
import { dirname } from "node:path"
|
|
15
|
+
import { dataDbPath } from "./config.ts"
|
|
16
|
+
|
|
17
|
+
export function openDataDb(): Database {
|
|
18
|
+
const path = dataDbPath()
|
|
19
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
20
|
+
return new Database(path, { create: true })
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 2121
|
|
2
|
+
// Singleton db handle holder + lifecycle orchestration. Decouples the db
|
|
3
|
+
// modules (which are now pure factories per `openX()`) from the consumers
|
|
4
|
+
// (index.ts, pipeline.ts) that need stable handle references throughout the
|
|
5
|
+
// HTTP request lifetime.
|
|
6
|
+
//
|
|
7
|
+
// Lifecycle (matches context/instance-spec.md §7):
|
|
8
|
+
//
|
|
9
|
+
// initDbs() — acquire runner lock, migrate runtime.db, open all three
|
|
10
|
+
// handles. Idempotent: second call is a no-op when handles
|
|
11
|
+
// are already populated.
|
|
12
|
+
// runtimeDb() — getter; throws when init has not run
|
|
13
|
+
// dataDb() — getter; throws when init has not run
|
|
14
|
+
// cacheDb() — getter; throws when init has not run
|
|
15
|
+
// closeDbs() — close all three handles, release the lock. Used by
|
|
16
|
+
// `startRunner`'s graceful stop path.
|
|
17
|
+
//
|
|
18
|
+
// Why getters and not exported instances: the db modules used to do their
|
|
19
|
+
// init at module load via top-level await, which forced lifecycle to happen
|
|
20
|
+
// during the import graph. That works for source-mod single-instance runs
|
|
21
|
+
// but breaks the moment startRunner() needs to set rootDir before init runs.
|
|
22
|
+
// Getters defer the resolution to call time; init runs once, before any
|
|
23
|
+
// HTTP request fires.
|
|
24
|
+
|
|
25
|
+
import type { Database } from "bun:sqlite"
|
|
26
|
+
import { openRuntimeDb } from "./runtime-db.ts"
|
|
27
|
+
import { openDataDb } from "./data-db.ts"
|
|
28
|
+
import { openCacheDb } from "./cache-db.ts"
|
|
29
|
+
import { acquireRunnerLock, releaseRunnerLock } from "./lock.ts"
|
|
30
|
+
|
|
31
|
+
let _runtimeDb: Database | null = null
|
|
32
|
+
let _dataDb: Database | null = null
|
|
33
|
+
let _cacheDb: Database | null = null
|
|
34
|
+
|
|
35
|
+
export async function initDbs(): Promise<void> {
|
|
36
|
+
if (_runtimeDb !== null) return // idempotent
|
|
37
|
+
|
|
38
|
+
await acquireRunnerLock()
|
|
39
|
+
_runtimeDb = openRuntimeDb()
|
|
40
|
+
_dataDb = openDataDb()
|
|
41
|
+
_cacheDb = openCacheDb()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function closeDbs(): Promise<void> {
|
|
45
|
+
if (_runtimeDb) { _runtimeDb.close(); _runtimeDb = null }
|
|
46
|
+
if (_dataDb) { _dataDb.close(); _dataDb = null }
|
|
47
|
+
if (_cacheDb) { _cacheDb.close(); _cacheDb = null }
|
|
48
|
+
await releaseRunnerLock()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runtimeDb(): Database {
|
|
52
|
+
if (_runtimeDb === null) {
|
|
53
|
+
throw new Error("[db-handles] runtimeDb() called before initDbs() — lifecycle violation")
|
|
54
|
+
}
|
|
55
|
+
return _runtimeDb
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function dataDb(): Database {
|
|
59
|
+
if (_dataDb === null) {
|
|
60
|
+
throw new Error("[db-handles] dataDb() called before initDbs() — lifecycle violation")
|
|
61
|
+
}
|
|
62
|
+
return _dataDb
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function cacheDb(): Database {
|
|
66
|
+
if (_cacheDb === null) {
|
|
67
|
+
throw new Error("[db-handles] cacheDb() called before initDbs() — lifecycle violation")
|
|
68
|
+
}
|
|
69
|
+
return _cacheDb
|
|
70
|
+
}
|