arcway 0.1.10 → 0.1.11

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/client/fetcher.js CHANGED
@@ -44,7 +44,7 @@ async function soloFetch(url, init) {
44
44
  throw new ApiError(res.status, 'UNKNOWN', json?.message || res.statusText);
45
45
  }
46
46
 
47
- return json.data;
47
+ return json;
48
48
  }
49
49
 
50
50
  export { ApiError, soloFetch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcway",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  "./ui/style-mira.css": "./client/ui/style-mira.css"
30
30
  },
31
31
  "scripts": {
32
+ "prepare": "esbuild client/router.jsx --outfile=client/router.js --format=esm --jsx=automatic",
32
33
  "test": "vitest run --project=unit",
33
34
  "test:storybook": "TMPDIR=~/tmp PLAYWRIGHT_BROWSERS_PATH=~/.cache/playwright vitest run --project=storybook",
34
35
  "test:all": "TMPDIR=~/tmp PLAYWRIGHT_BROWSERS_PATH=~/.cache/playwright vitest run",
@@ -37,7 +38,8 @@
37
38
  "lint": "eslint server/ client/ tests/",
38
39
  "lint:fix": "eslint server/ client/ tests/ --fix",
39
40
  "storybook": "storybook dev -p 6006",
40
- "build-storybook": "storybook build"
41
+ "build-storybook": "storybook build",
42
+ "deploy:docs": "cd ~/Projects/home-server/baremetal/ansible && ansible-playbook deploy-arcway-docs.yml"
41
43
  },
42
44
  "dependencies": {
43
45
  "@aws-sdk/client-s3": "^3.987.0",
@@ -6,7 +6,10 @@ import { createDB } from '#server/db/index.js';
6
6
  async function runMigrateMake(name) {
7
7
  const rootDir = process.cwd();
8
8
  try {
9
- const migrationsDir = path.resolve(rootDir, 'migrations');
9
+ const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
10
+ loadEnvFiles(rootDir, mode);
11
+ const config = await makeConfig(rootDir);
12
+ const migrationsDir = config.database.dir;
10
13
  await fs.mkdir(migrationsDir, { recursive: true });
11
14
  const now = new Date();
12
15
  const timestamp = [
@@ -1,4 +1,3 @@
1
- import path from 'node:path';
2
1
  import { makeConfig } from '#server/config/loader.js';
3
2
  import { loadEnvFiles } from '#server/env.js';
4
3
  import { createDB } from '#server/db/index.js';
@@ -11,7 +10,7 @@ async function runSeed() {
11
10
  const config = await makeConfig(rootDir);
12
11
  const db = await createDB(config.database);
13
12
  await db.runMigrations();
14
- const seedsDir = path.join(rootDir, 'seeds');
13
+ const seedsDir = config.seeds.dir;
15
14
  const results = await runSeeds(db, seedsDir);
16
15
  if (results.length === 0) {
17
16
  console.log('No seed files found.');
@@ -17,6 +17,7 @@ import resolvePages from './modules/pages.js';
17
17
  import resolveBuild from './modules/build.js';
18
18
  import resolveMcp from './modules/mcp.js';
19
19
  import resolveWebsocket from './modules/websocket.js';
20
+ import resolveSeeds from './modules/seeds.js';
20
21
 
21
22
  function deepMerge(target, source) {
22
23
  const result = { ...target };
@@ -53,6 +54,7 @@ const modules = [
53
54
  resolveBuild,
54
55
  resolveMcp,
55
56
  resolveWebsocket,
57
+ resolveSeeds,
56
58
  ];
57
59
 
58
60
  async function makeConfig(rootDir, { overrides, mode } = {}) {
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
 
3
3
  const DEFAULTS = {
4
4
  sqliteFilename: '.build/db/arcway.db',
5
- migrationsDir: 'migrations',
5
+ dir: 'migrations',
6
6
  };
7
7
 
8
8
  const CLIENT_MAP = {
@@ -15,8 +15,8 @@ function resolve(config, { rootDir } = {}) {
15
15
  const db = { ...DEFAULTS, ...config.database };
16
16
  // Resolve friendly client names to actual knex driver names
17
17
  db.client = CLIENT_MAP[db.client] ?? db.client;
18
- if (db.migrationsDir && !path.isAbsolute(db.migrationsDir)) {
19
- db.migrationsDir = path.resolve(rootDir, db.migrationsDir);
18
+ if (db.dir && !path.isAbsolute(db.dir)) {
19
+ db.dir = path.resolve(rootDir, db.dir);
20
20
  }
21
21
  const isSqlite = db.client === 'better-sqlite3' || db.client === 'sqlite3';
22
22
  if (isSqlite && !db.connection) {
@@ -1,13 +1,13 @@
1
1
  import path from 'node:path';
2
2
 
3
3
  const DEFAULTS = {
4
- listenersDir: 'listeners',
4
+ dir: 'listeners',
5
5
  };
6
6
 
7
7
  function resolve(config, { rootDir } = {}) {
8
8
  const events = { ...DEFAULTS, ...config.events };
9
- if (events.listenersDir && !path.isAbsolute(events.listenersDir)) {
10
- events.listenersDir = path.resolve(rootDir, events.listenersDir);
9
+ if (events.dir && !path.isAbsolute(events.dir)) {
10
+ events.dir = path.resolve(rootDir, events.dir);
11
11
  }
12
12
  return { ...config, events };
13
13
  }
@@ -6,13 +6,13 @@ const DEFAULTS = {
6
6
  pollIntervalMs: 60000,
7
7
  tableName: 'arcway_jobs',
8
8
  leaseTable: 'arcway_job_leases',
9
- jobsDir: 'jobs',
9
+ dir: 'jobs',
10
10
  };
11
11
 
12
12
  function resolve(config, { rootDir } = {}) {
13
13
  const jobs = { ...DEFAULTS, ...config.jobs };
14
- if (jobs.jobsDir && !path.isAbsolute(jobs.jobsDir)) {
15
- jobs.jobsDir = path.resolve(rootDir, jobs.jobsDir);
14
+ if (jobs.dir && !path.isAbsolute(jobs.dir)) {
15
+ jobs.dir = path.resolve(rootDir, jobs.dir);
16
16
  }
17
17
  return { ...config, jobs };
18
18
  }
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+
3
+ const DEFAULTS = {
4
+ dir: 'seeds',
5
+ };
6
+
7
+ function resolve(config, { rootDir } = {}) {
8
+ const seeds = { ...DEFAULTS, ...config.seeds };
9
+ if (seeds.dir && !path.isAbsolute(seeds.dir)) {
10
+ seeds.dir = path.resolve(rootDir, seeds.dir);
11
+ }
12
+ return { ...config, seeds };
13
+ }
14
+
15
+ export default resolve;
@@ -45,6 +45,12 @@ async function createDB(config, { log } = {}) {
45
45
  throw new Error(`Database connection failed: ${err}`);
46
46
  }
47
47
 
48
+ if (isSqlite) {
49
+ await db.raw('PRAGMA journal_mode = WAL');
50
+ await db.raw('PRAGMA busy_timeout = 5000');
51
+ await db.raw('PRAGMA synchronous = NORMAL');
52
+ }
53
+
48
54
  const origDestroy = db.destroy.bind(db);
49
55
  Object.defineProperty(db, 'destroy', {
50
56
  value: async () => {
@@ -56,7 +62,7 @@ async function createDB(config, { log } = {}) {
56
62
  });
57
63
 
58
64
  db.runMigrations = async () => {
59
- const migrationsDir = config.migrationsDir;
65
+ const migrationsDir = config.dir;
60
66
  if (!migrationsDir) return;
61
67
  const [batch, migrations] = await db.migrate.latest({
62
68
  migrationSource: new MigrationSource(migrationsDir),
@@ -69,8 +75,8 @@ async function createDB(config, { log } = {}) {
69
75
  };
70
76
 
71
77
  db.runRollback = async () => {
72
- const migrationsDir = config.migrationsDir;
73
- if (!migrationsDir) throw new Error('No migrationsDir configured');
78
+ const migrationsDir = config.dir;
79
+ if (!migrationsDir) throw new Error('No migrations dir configured');
74
80
  const [batch, entries] = await db.migrate.rollback({
75
81
  migrationSource: new MigrationSource(migrationsDir),
76
82
  });
@@ -6,34 +6,22 @@ class MemoryTransport {
6
6
  const regex = patternToRegex(pattern);
7
7
  this.subscriptions.push({ pattern, regex, handler });
8
8
  }
9
- /**
10
- * Emit an event. All matching subscribers are called concurrently.
11
- * Errors in individual listeners are caught and collected — one failing
12
- * listener does not prevent others from running.
13
- */
9
+ /** Emit an event. Handlers run asynchronously — fire-and-forget. */
14
10
  async emit(eventName, payload) {
15
11
  const matching = this.subscriptions.filter((sub) => sub.regex.test(eventName));
16
- if (matching.length === 0) {
17
- return;
18
- }
19
- const errors = [];
20
- const results = await Promise.allSettled(
12
+ if (matching.length === 0) return;
13
+ Promise.allSettled(
21
14
  matching.map((sub) => sub.handler(eventName, payload)),
22
- );
23
- for (let i = 0; i < results.length; i++) {
24
- const result = results[i];
25
- if (result.status === 'rejected') {
26
- const sub = matching[i];
27
- errors.push({ pattern: sub.pattern, error: result.reason });
28
- console.error(
29
- `Event listener error [${sub.pattern}] for event "${eventName}":`,
30
- result.reason,
31
- );
15
+ ).then((results) => {
16
+ for (let i = 0; i < results.length; i++) {
17
+ if (results[i].status === 'rejected') {
18
+ console.error(
19
+ `Event listener error [${matching[i].pattern}] for event "${eventName}":`,
20
+ results[i].reason,
21
+ );
22
+ }
32
23
  }
33
- }
34
- if (errors.length > 0 && errors.length === matching.length) {
35
- throw new Error(`All ${errors.length} listener(s) for event "${eventName}" failed`);
36
- }
24
+ });
37
25
  }
38
26
  async disconnect() {}
39
27
 
@@ -20,13 +20,13 @@ function validateHandler(item, filePath, index) {
20
20
 
21
21
  class EventHandler {
22
22
  _events;
23
- _listenersDir;
23
+ _dir;
24
24
  _log;
25
25
  _appContext;
26
26
  _listeners = [];
27
27
 
28
28
  constructor(config, { events, log, appContext } = {}) {
29
- this._listenersDir = config?.listenersDir;
29
+ this._dir = config?.dir;
30
30
  this._events = events;
31
31
  this._log = log;
32
32
  this._appContext = appContext ?? {
@@ -41,8 +41,8 @@ class EventHandler {
41
41
  }
42
42
 
43
43
  async init() {
44
- if (!this._listenersDir) return;
45
- const entries = await discoverModules(this._listenersDir, {
44
+ if (!this._dir) return;
45
+ const entries = await discoverModules(this._dir, {
46
46
  recursive: true,
47
47
  label: 'listener file',
48
48
  });
@@ -59,7 +59,7 @@ class JobRunner {
59
59
  }
60
60
 
61
61
  async init() {
62
- const jobsDir = this._config?.jobsDir;
62
+ const jobsDir = this._config?.dir;
63
63
  if (!jobsDir) return;
64
64
 
65
65
  // Discover and register user jobs
@@ -46,7 +46,7 @@ const validateRequest = validateRequestSchema;
46
46
  async function serializeResponse(res, response, responseHeaders, statusCode) {
47
47
  const customContentType = responseHeaders['Content-Type'] || responseHeaders['content-type'];
48
48
  if (!customContentType || customContentType.includes('application/json')) {
49
- const responseBody = response.error ? { error: response.error } : { data: response.data };
49
+ const responseBody = response.error ? { error: response.error } : (response.data ?? null);
50
50
  sendJson(res, statusCode, responseBody, responseHeaders);
51
51
  return;
52
52
  }
@@ -128,9 +128,9 @@ async function createTestContext(domainName, options) {
128
128
  if (options?.rootDir) {
129
129
  const migrationsDir = path.join(options.rootDir, 'migrations');
130
130
  await db.migrate.latest({ migrationSource: new MigrationSource(migrationsDir) });
131
- } else if (options?.migrationsDir) {
131
+ } else if (options?.dir || options?.migrationsDir) {
132
132
  await db.migrate.latest({
133
- migrationSource: new MigrationSource(options.migrationsDir),
133
+ migrationSource: new MigrationSource(options.dir ?? options.migrationsDir),
134
134
  });
135
135
  }
136
136
  const scopedDb = db;