arcway 0.1.26 → 0.1.28
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 +4 -1
- package/client/hooks/swr-compat.js +4 -2
- package/client/hooks/use-api-paginated.js +74 -0
- package/client/index.js +1 -0
- package/package.json +4 -3
- package/server/boot/index.js +22 -6
- package/server/build.js +73 -4
- package/server/cache/index.js +12 -2
- package/server/cache/ttl.js +24 -0
- package/server/config/duration.js +49 -0
- package/server/config/loader.js +4 -0
- package/server/config/modules/cache.js +7 -1
- package/server/config/modules/jobs.js +14 -1
- package/server/config/modules/mail.js +16 -1
- package/server/config/modules/mcp.js +5 -1
- package/server/config/modules/pages.js +1 -0
- package/server/config/modules/plugins.js +18 -0
- package/server/config/modules/queue.js +7 -1
- package/server/config/modules/secrets.js +145 -0
- package/server/config/modules/server.js +13 -1
- package/server/config/modules/session.js +5 -1
- package/server/config/modules/websocket.js +8 -0
- package/server/context.js +61 -2
- package/server/db/index.js +39 -9
- package/server/discovery.js +45 -3
- package/server/events/drivers/memory.js +22 -4
- package/server/events/drivers/redis.js +20 -5
- package/server/events/handler.js +21 -7
- package/server/events/index.js +2 -2
- package/server/index.js +2 -1
- package/server/internals.js +8 -0
- package/server/jobs/drivers/run-handler.js +3 -0
- package/server/jobs/runner.js +13 -4
- package/server/jobs/worker-config.js +58 -0
- package/server/jobs/worker-entry.js +7 -12
- package/server/lib/vault/index.js +20 -0
- package/server/lib/vault/secrets.js +149 -0
- package/server/meta.js +106 -0
- package/server/pages/build-plugins.js +2 -1
- package/server/pages/handler.js +23 -5
- package/server/pages/out-dir.js +103 -2
- package/server/pages/pages-router.js +3 -2
- package/server/pages/ssr.js +55 -18
- package/server/plugins/capabilities.js +66 -0
- package/server/plugins/discovery.js +111 -0
- package/server/plugins/graph.js +114 -0
- package/server/plugins/manager.js +210 -0
- package/server/plugins/manifest.js +94 -0
- package/server/router/api-router.js +41 -16
- package/server/router/routes.js +14 -2
- package/server/server.js +13 -1
- package/server/session/index.js +5 -1
- package/server/static/index.js +5 -2
- package/server/web-server.js +3 -3
- package/server/ws/realtime.js +24 -8
- package/server/ws/ws-router.js +24 -8
package/client/fetcher.js
CHANGED
|
@@ -2,13 +2,15 @@ class ApiError extends Error {
|
|
|
2
2
|
status;
|
|
3
3
|
code;
|
|
4
4
|
details;
|
|
5
|
+
data;
|
|
5
6
|
|
|
6
|
-
constructor(status, code, message, details) {
|
|
7
|
+
constructor(status, code, message, details, data) {
|
|
7
8
|
super(message);
|
|
8
9
|
this.name = 'ApiError';
|
|
9
10
|
this.status = status;
|
|
10
11
|
this.code = code;
|
|
11
12
|
this.details = details;
|
|
13
|
+
this.data = data;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -39,6 +41,7 @@ async function soloFetch(url, init) {
|
|
|
39
41
|
json.error.code || 'UNKNOWN',
|
|
40
42
|
json.error.message || res.statusText,
|
|
41
43
|
json.error.details,
|
|
44
|
+
json.data,
|
|
42
45
|
);
|
|
43
46
|
}
|
|
44
47
|
throw new ApiError(res.status, 'UNKNOWN', json?.message || res.statusText);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as SWR from 'swr';
|
|
2
|
+
import * as SWRInfinite from 'swr/infinite';
|
|
2
3
|
import * as SWRMutation from 'swr/mutation';
|
|
3
4
|
|
|
4
5
|
function resolveHook(mod) {
|
|
@@ -15,11 +16,12 @@ function resolveNamed(mod, key) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const useSWR = resolveHook(SWR);
|
|
19
|
+
const useSWRInfinite = resolveHook(SWRInfinite);
|
|
18
20
|
const useSWRConfig = resolveNamed(SWR, 'useSWRConfig');
|
|
19
21
|
const useSWRMutation = resolveHook(SWRMutation);
|
|
20
22
|
|
|
21
|
-
if (!useSWR || !useSWRConfig || !useSWRMutation) {
|
|
23
|
+
if (!useSWR || !useSWRInfinite || !useSWRConfig || !useSWRMutation) {
|
|
22
24
|
throw new Error('Arcway failed to resolve SWR hooks');
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
export { useSWR, useSWRConfig, useSWRMutation };
|
|
27
|
+
export { useSWR, useSWRInfinite, useSWRConfig, useSWRMutation };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { useApiContext } from '../provider.js';
|
|
3
|
+
import { soloFetch } from '../fetcher.js';
|
|
4
|
+
import { stringifyQuery } from '../query.js';
|
|
5
|
+
import { useSWRConfig, useSWRInfinite } from './swr-compat.js';
|
|
6
|
+
|
|
7
|
+
function buildUrl(base, query) {
|
|
8
|
+
if (!query) return base;
|
|
9
|
+
const qs = stringifyQuery(query);
|
|
10
|
+
return qs ? `${base}?${qs}` : base;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function useApiPaginated(path, getQuery, options) {
|
|
14
|
+
const { pathPrefix, headers } = useApiContext();
|
|
15
|
+
const { mutate: globalMutate } = useSWRConfig();
|
|
16
|
+
const getQueryRef = useRef(getQuery);
|
|
17
|
+
getQueryRef.current = getQuery;
|
|
18
|
+
|
|
19
|
+
const disabled = path === null || options?.disable === true;
|
|
20
|
+
const fullPath = disabled ? null : `${pathPrefix}${path}`;
|
|
21
|
+
const swrConfig = useMemo(() => {
|
|
22
|
+
const { disable: _disable, ws: _ws, ...rest } = options ?? {};
|
|
23
|
+
return rest;
|
|
24
|
+
}, [options]);
|
|
25
|
+
|
|
26
|
+
const getKey = useCallback(
|
|
27
|
+
(pageIndex, previousPageData) => {
|
|
28
|
+
if (!fullPath) return null;
|
|
29
|
+
|
|
30
|
+
const query =
|
|
31
|
+
typeof getQueryRef.current === 'function'
|
|
32
|
+
? getQueryRef.current(pageIndex, previousPageData)
|
|
33
|
+
: getQueryRef.current;
|
|
34
|
+
|
|
35
|
+
if (query === null) return null;
|
|
36
|
+
return buildUrl(fullPath, query);
|
|
37
|
+
},
|
|
38
|
+
[fullPath],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const result = useSWRInfinite(
|
|
42
|
+
getKey,
|
|
43
|
+
(key) => soloFetch(key, { headers }),
|
|
44
|
+
swrConfig,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const makeMethod = useCallback(
|
|
48
|
+
(method) => async (body) => {
|
|
49
|
+
if (!fullPath) throw new Error('Cannot call mutation on a disabled useApiPaginated hook');
|
|
50
|
+
|
|
51
|
+
const res = await soloFetch(fullPath, {
|
|
52
|
+
method,
|
|
53
|
+
headers: {
|
|
54
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
55
|
+
...headers,
|
|
56
|
+
},
|
|
57
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await globalMutate((key) => typeof key === 'string' && key.startsWith(fullPath));
|
|
61
|
+
return res;
|
|
62
|
+
},
|
|
63
|
+
[fullPath, headers, globalMutate],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...result,
|
|
68
|
+
loading: result.isLoading,
|
|
69
|
+
post: makeMethod('POST'),
|
|
70
|
+
put: makeMethod('PUT'),
|
|
71
|
+
patch: makeMethod('PATCH'),
|
|
72
|
+
del: makeMethod('DELETE'),
|
|
73
|
+
};
|
|
74
|
+
}
|
package/client/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export {
|
|
|
7
7
|
useWsManager,
|
|
8
8
|
} from './provider.js';
|
|
9
9
|
export { default as useApi } from './hooks/use-api.js';
|
|
10
|
+
export { default as useApiPaginated } from './hooks/use-api-paginated.js';
|
|
10
11
|
export { default as useQuery } from './hooks/use-query.js';
|
|
11
12
|
export { default as useMutation } from './hooks/use-mutation.js';
|
|
12
13
|
export { default as useDebounce } from './hooks/use-debounce.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcway",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "A convention-based framework for building modular monoliths with strict domain boundaries.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@aws-sdk/client-s3": "^3.987.0",
|
|
49
49
|
"@babel/core": "^7.29.0",
|
|
50
|
-
"@vitejs/plugin-react": "^5.1.0",
|
|
51
50
|
"@base-ui/react": "^1.2.0",
|
|
52
51
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
53
|
-
"@tailwindcss/vite": "^4.1.18",
|
|
54
52
|
"@tailwindcss/postcss": "^4.1.18",
|
|
53
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
54
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
55
55
|
"arktype": "^2.1.29",
|
|
56
56
|
"bcryptjs": "^3.0.3",
|
|
57
57
|
"better-sqlite3": "^12.6.2",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"knex": "^3.1.0",
|
|
81
81
|
"lucide-react": "^0.564.0",
|
|
82
82
|
"mailparser": "^3.9.3",
|
|
83
|
+
"ms": "^2.1.3",
|
|
83
84
|
"nanoid": "^5.1.6",
|
|
84
85
|
"negotiator": "^1.0.0",
|
|
85
86
|
"nodemailer": "^8.0.1",
|
package/server/boot/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createInfrastructure, destroyInfrastructure } from './infrastructure.js
|
|
|
3
3
|
import { EventHandler } from '../events/handler.js';
|
|
4
4
|
import { JobRunner } from '../jobs/runner.js';
|
|
5
5
|
import { WorkerPool } from '../jobs/worker-pool.js';
|
|
6
|
+
import { createWorkerData } from '../jobs/worker-config.js';
|
|
6
7
|
import { loadEnvFiles } from '../env.js';
|
|
7
8
|
import { McpRouter } from '../mcp/index.js';
|
|
8
9
|
import WebServer from '../web-server.js';
|
|
@@ -16,6 +17,8 @@ import { createWsBackplane } from '../ws/backplane.js';
|
|
|
16
17
|
import { runHook } from './hooks.js';
|
|
17
18
|
import { createViteDevRouter } from '../pages/vite-dev.js';
|
|
18
19
|
import { resolvePagesOutDir } from '../pages/out-dir.js';
|
|
20
|
+
import { collectRuntimeMeta } from '../meta.js';
|
|
21
|
+
import { createPluginManager } from '../plugins/manager.js';
|
|
19
22
|
import path from 'node:path';
|
|
20
23
|
|
|
21
24
|
async function boot(options) {
|
|
@@ -25,8 +28,12 @@ async function boot(options) {
|
|
|
25
28
|
const envFiles = loadEnvFiles(rootDir, mode);
|
|
26
29
|
const config = await makeConfig(rootDir, { overrides: options.configOverrides, mode });
|
|
27
30
|
|
|
31
|
+
const plugins = await createPluginManager(config.plugins, { runLifecycle: false });
|
|
32
|
+
config.database.pluginMigrations = plugins.migrations();
|
|
33
|
+
|
|
28
34
|
const infrastructure = await createInfrastructure(config, { runMigrations: true });
|
|
29
35
|
const { db, redis, queue, cache, files, mail, events, log } = infrastructure;
|
|
36
|
+
const meta = await collectRuntimeMeta(rootDir);
|
|
30
37
|
|
|
31
38
|
if (envFiles.length > 0) log.info('Env files loaded', { envFiles });
|
|
32
39
|
|
|
@@ -37,7 +44,12 @@ async function boot(options) {
|
|
|
37
44
|
// framework services and to the init/ready/shutdown hooks. Mutations from
|
|
38
45
|
// the init hook (e.g. wrapping `appContext.db`) persist for process lifetime
|
|
39
46
|
// because downstream consumers read properties off this reference at use time.
|
|
40
|
-
const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher };
|
|
47
|
+
const appContext = { db, redis, events, queue, cache, files, mail, log, fileWatcher, meta };
|
|
48
|
+
|
|
49
|
+
plugins.log = log;
|
|
50
|
+
appContext.plugins = plugins;
|
|
51
|
+
|
|
52
|
+
await plugins.runEnabledLifecycle();
|
|
41
53
|
|
|
42
54
|
await runHook('init', appContext, rootDir);
|
|
43
55
|
|
|
@@ -50,14 +62,14 @@ async function boot(options) {
|
|
|
50
62
|
});
|
|
51
63
|
await eventHandler.init();
|
|
52
64
|
|
|
53
|
-
// Workers rebuild their own infrastructure
|
|
54
|
-
// so
|
|
55
|
-
//
|
|
65
|
+
// Workers rebuild their own infrastructure. workerData must stay structured-
|
|
66
|
+
// clone safe, so function-valued app config hooks are stripped from the
|
|
67
|
+
// snapshot and reloaded from arcway.config.js inside worker-entry.
|
|
56
68
|
const workerPool =
|
|
57
69
|
config.jobs?.workerPoolSize > 0
|
|
58
70
|
? new WorkerPool({
|
|
59
71
|
size: config.jobs.workerPoolSize,
|
|
60
|
-
workerData: { config },
|
|
72
|
+
workerData: createWorkerData({ config, rootDir, mode, meta }),
|
|
61
73
|
defaultTimeoutMs: config.jobs?.staleTimeoutMs ?? null,
|
|
62
74
|
})
|
|
63
75
|
: null;
|
|
@@ -70,7 +82,9 @@ async function boot(options) {
|
|
|
70
82
|
mail,
|
|
71
83
|
events,
|
|
72
84
|
log,
|
|
85
|
+
meta,
|
|
73
86
|
workerPool,
|
|
87
|
+
plugins,
|
|
74
88
|
});
|
|
75
89
|
await jobRunner.init();
|
|
76
90
|
|
|
@@ -87,7 +101,8 @@ async function boot(options) {
|
|
|
87
101
|
|
|
88
102
|
const pagesRouter = new PagesRouter(config, { rootDir, log, mode, fileWatcher, appContext });
|
|
89
103
|
await pagesRouter.init();
|
|
90
|
-
const
|
|
104
|
+
const resolveCurrentPagesOutDir = () => resolvePagesOutDir(config, rootDir, mode);
|
|
105
|
+
const pagesOutDir = resolveCurrentPagesOutDir();
|
|
91
106
|
const viteRouter =
|
|
92
107
|
mode === 'development' && config.pages?.vite?.enabled === true
|
|
93
108
|
? await createViteDevRouter({ rootDir, log, config })
|
|
@@ -101,6 +116,7 @@ async function boot(options) {
|
|
|
101
116
|
|
|
102
117
|
const staticRouter = new StaticRouter({
|
|
103
118
|
outDir: pagesOutDir,
|
|
119
|
+
...(mode === 'production' ? { resolveOutDir: resolveCurrentPagesOutDir } : {}),
|
|
104
120
|
publicDir: path.join(rootDir, 'public'),
|
|
105
121
|
});
|
|
106
122
|
|
package/server/build.js
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { makeConfig } from './config/loader.js';
|
|
2
4
|
import { buildPages } from './pages/build.js';
|
|
3
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createPagesVersionedBuildTarget,
|
|
7
|
+
pruneOldPagesBuilds,
|
|
8
|
+
resolvePagesBaseOutDir,
|
|
9
|
+
} from './pages/out-dir.js';
|
|
10
|
+
import { discoverRoutes } from './router/routes.js';
|
|
11
|
+
import { discoverMiddleware } from './router/middleware.js';
|
|
12
|
+
import { EventHandler } from './events/handler.js';
|
|
13
|
+
import { discoverJobs } from './jobs/runner.js';
|
|
14
|
+
import { collectRuntimeMeta } from './meta.js';
|
|
15
|
+
import { createPluginManager } from './plugins/manager.js';
|
|
16
|
+
|
|
17
|
+
async function writeBuildMeta(rootDir, meta) {
|
|
18
|
+
const buildDir = path.join(rootDir, '.build');
|
|
19
|
+
const targetPath = path.join(buildDir, 'meta.json');
|
|
20
|
+
const tempPath = path.join(buildDir, `meta.json.next-${process.pid}`);
|
|
21
|
+
await fs.mkdir(buildDir, { recursive: true });
|
|
22
|
+
await fs.writeFile(tempPath, JSON.stringify(meta, null, 2));
|
|
23
|
+
await fs.rename(tempPath, targetPath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function validateServerModules(config) {
|
|
27
|
+
const plugins = await createPluginManager(config.plugins, { runLifecycle: false });
|
|
28
|
+
if (config?.api?.enabled !== false && config?.api?.dir) {
|
|
29
|
+
await discoverRoutes(config.api.dir);
|
|
30
|
+
await discoverMiddleware(config.api.dir);
|
|
31
|
+
await plugins.discoverRoutes();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (config?.events?.dir) {
|
|
35
|
+
const handler = new EventHandler(
|
|
36
|
+
{ dir: config.events.dir },
|
|
37
|
+
{
|
|
38
|
+
events: { subscribe() {} },
|
|
39
|
+
log: {
|
|
40
|
+
info() {},
|
|
41
|
+
warn() {},
|
|
42
|
+
error() {},
|
|
43
|
+
extend() {
|
|
44
|
+
return this;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
await handler.init();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (config?.jobs?.enabled !== false && config?.jobs?.dir) {
|
|
53
|
+
await discoverJobs(config.jobs.dir);
|
|
54
|
+
await plugins.discoverJobs();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
4
57
|
|
|
5
58
|
async function build(options) {
|
|
6
59
|
const rootDir = options?.rootDir ?? process.cwd();
|
|
@@ -10,16 +63,32 @@ async function build(options) {
|
|
|
10
63
|
let pages;
|
|
11
64
|
const pagesEnabled = config?.pages?.enabled !== false;
|
|
12
65
|
if (pagesEnabled) {
|
|
66
|
+
const baseOutDir = resolvePagesBaseOutDir(config, rootDir, 'production');
|
|
67
|
+
const target = await createPagesVersionedBuildTarget(baseOutDir);
|
|
13
68
|
pages = await buildPages({
|
|
14
69
|
rootDir,
|
|
15
|
-
outDir:
|
|
70
|
+
outDir: target.outDir,
|
|
16
71
|
serverTarget: buildTarget,
|
|
17
72
|
minify: true,
|
|
18
73
|
fonts: config?.pages?.fonts,
|
|
19
74
|
});
|
|
75
|
+
await pruneOldPagesBuilds(baseOutDir, config?.pages?.retainBuilds ?? 5);
|
|
76
|
+
pages = {
|
|
77
|
+
...pages,
|
|
78
|
+
baseOutDir,
|
|
79
|
+
buildHash: target.buildHash,
|
|
80
|
+
outDir: target.outDir,
|
|
81
|
+
};
|
|
20
82
|
}
|
|
21
83
|
|
|
22
|
-
|
|
84
|
+
await validateServerModules(config);
|
|
85
|
+
const meta = await collectRuntimeMeta(rootDir);
|
|
86
|
+
await writeBuildMeta(rootDir, {
|
|
87
|
+
...meta,
|
|
88
|
+
...(pages?.buildHash ? { pagesBuildHash: pages.buildHash } : {}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { pages, serverValidated: true, meta };
|
|
23
92
|
}
|
|
24
93
|
|
|
25
|
-
export { build };
|
|
94
|
+
export { build, validateServerModules };
|
package/server/cache/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import MemoryCacheDriver from './drivers/memory.js';
|
|
2
2
|
import RedisCacheDriver from './drivers/redis.js';
|
|
3
|
+
import { normalizeCacheTtl } from './ttl.js';
|
|
3
4
|
|
|
4
5
|
class Cache {
|
|
5
6
|
_driver;
|
|
@@ -9,6 +10,10 @@ class Cache {
|
|
|
9
10
|
constructor(config, { redis, log } = {}) {
|
|
10
11
|
this._log = log;
|
|
11
12
|
this._namespace = config?.namespace ?? '';
|
|
13
|
+
this._defaultTtlMs =
|
|
14
|
+
config?.defaultTtlMs === undefined
|
|
15
|
+
? undefined
|
|
16
|
+
: normalizeCacheTtl(config.defaultTtlMs, 'cache.defaultTtlMs');
|
|
12
17
|
|
|
13
18
|
if (config?.driver === 'redis') {
|
|
14
19
|
this._driver = new RedisCacheDriver(redis.client, redis.keyPrefix);
|
|
@@ -35,7 +40,9 @@ class Cache {
|
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
async set(key, value, ttlMs) {
|
|
38
|
-
|
|
43
|
+
const resolvedTtlMs =
|
|
44
|
+
ttlMs === undefined ? this._defaultTtlMs : normalizeCacheTtl(ttlMs, 'cache ttl');
|
|
45
|
+
await this._driver.set(this._namespace, key, JSON.stringify(value), resolvedTtlMs);
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
async delete(key) {
|
|
@@ -52,7 +59,9 @@ class Cache {
|
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
const value = await fn();
|
|
55
|
-
|
|
62
|
+
const resolvedTtlMs =
|
|
63
|
+
ttlMs === undefined ? this._defaultTtlMs : normalizeCacheTtl(ttlMs, 'cache ttl');
|
|
64
|
+
await this._driver.set(this._namespace, key, JSON.stringify(value), resolvedTtlMs);
|
|
56
65
|
return value;
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -62,6 +71,7 @@ class Cache {
|
|
|
62
71
|
child._driver = this._driver;
|
|
63
72
|
child._log = this._log;
|
|
64
73
|
child._namespace = namespace;
|
|
74
|
+
child._defaultTtlMs = this._defaultTtlMs;
|
|
65
75
|
return child;
|
|
66
76
|
}
|
|
67
77
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseDuration } from '../config/duration.js'
|
|
2
|
+
|
|
3
|
+
function parseCacheTtl(value) {
|
|
4
|
+
if (value === undefined || value === null) return null
|
|
5
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
|
6
|
+
if (typeof value !== 'string') return null
|
|
7
|
+
|
|
8
|
+
const normalized = value.trim()
|
|
9
|
+
if (!normalized) return null
|
|
10
|
+
|
|
11
|
+
return parseDuration(normalized)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeCacheTtl(value, label = 'cache ttl') {
|
|
15
|
+
const parsed = parseCacheTtl(value)
|
|
16
|
+
if (parsed === null) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Invalid ${label}: ${JSON.stringify(value)}. Use milliseconds as a number or a string like "500ms", "30s", "15m", "2h", or "7d".`,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
return parsed
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { parseCacheTtl, normalizeCacheTtl }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import ms from 'ms'
|
|
2
|
+
|
|
3
|
+
function parseDuration(value) {
|
|
4
|
+
if (value === undefined || value === null) return null
|
|
5
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
|
6
|
+
if (typeof value !== 'string') return null
|
|
7
|
+
|
|
8
|
+
const normalized = value.trim()
|
|
9
|
+
if (!normalized) return null
|
|
10
|
+
|
|
11
|
+
const parsed = ms(normalized)
|
|
12
|
+
return typeof parsed === 'number' ? parsed : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeDurationMs(value, label) {
|
|
16
|
+
const parsed = parseDuration(value)
|
|
17
|
+
if (parsed === null) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Invalid ${label}: ${JSON.stringify(value)}. Use milliseconds as a number or a string like "500ms", "30s", "15m", "2h", or "7d".`,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
return parsed
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeDurationSeconds(value, label) {
|
|
26
|
+
if (value === 0) return 0
|
|
27
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
|
28
|
+
const parsed = parseDuration(value)
|
|
29
|
+
if (parsed === null) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid ${label}: ${JSON.stringify(value)}. Use seconds as a number or a string like "30s", "15m", "2h", or "7d".`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return Math.floor(parsed / 1000)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeDurationDays(value, label) {
|
|
38
|
+
if (value === null) return null
|
|
39
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
|
40
|
+
const parsed = parseDuration(value)
|
|
41
|
+
if (parsed === null) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Invalid ${label}: ${JSON.stringify(value)}. Use days as a number or a string like "7d", "2w", or "168h".`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return Math.floor(parsed / 86_400_000)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { parseDuration, normalizeDurationMs, normalizeDurationSeconds, normalizeDurationDays }
|
package/server/config/loader.js
CHANGED
|
@@ -12,12 +12,14 @@ import resolveRedis from './modules/redis.js';
|
|
|
12
12
|
import resolveCache from './modules/cache.js';
|
|
13
13
|
import resolveEvents from './modules/events.js';
|
|
14
14
|
import resolveMail from './modules/mail.js';
|
|
15
|
+
import resolveSecrets from './modules/secrets.js';
|
|
15
16
|
import resolveSession from './modules/session.js';
|
|
16
17
|
import resolvePages from './modules/pages.js';
|
|
17
18
|
import resolveBuild from './modules/build.js';
|
|
18
19
|
import resolveMcp from './modules/mcp.js';
|
|
19
20
|
import resolveWebsocket from './modules/websocket.js';
|
|
20
21
|
import resolveSeeds from './modules/seeds.js';
|
|
22
|
+
import resolvePlugins from './modules/plugins.js';
|
|
21
23
|
|
|
22
24
|
function deepMerge(target, source) {
|
|
23
25
|
const result = { ...target };
|
|
@@ -49,12 +51,14 @@ const modules = [
|
|
|
49
51
|
resolveFiles,
|
|
50
52
|
resolveMail,
|
|
51
53
|
resolveRedis,
|
|
54
|
+
resolveSecrets,
|
|
52
55
|
resolveSession,
|
|
53
56
|
resolvePages,
|
|
54
57
|
resolveBuild,
|
|
55
58
|
resolveMcp,
|
|
56
59
|
resolveWebsocket,
|
|
57
60
|
resolveSeeds,
|
|
61
|
+
resolvePlugins,
|
|
58
62
|
];
|
|
59
63
|
|
|
60
64
|
async function makeConfig(rootDir, { overrides, mode } = {}) {
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { normalizeCacheTtl } from '../../cache/ttl.js';
|
|
2
|
+
|
|
1
3
|
const DEFAULTS = {
|
|
2
4
|
driver: 'memory',
|
|
3
5
|
};
|
|
4
6
|
|
|
5
7
|
function resolve(config) {
|
|
6
8
|
if (!config.cache) return config;
|
|
7
|
-
|
|
9
|
+
const resolved = { ...DEFAULTS, ...config.cache };
|
|
10
|
+
if (resolved.defaultTtlMs !== undefined) {
|
|
11
|
+
resolved.defaultTtlMs = normalizeCacheTtl(resolved.defaultTtlMs, 'cache.defaultTtlMs');
|
|
12
|
+
}
|
|
13
|
+
return { ...config, cache: resolved };
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
export default resolve;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { normalizeDurationMs } from '../duration.js';
|
|
3
4
|
|
|
4
5
|
// Leave one core free for the main thread (dispatch/polling/HTTP). 0 disables
|
|
5
6
|
// worker threads entirely and runs handlers inline on the main thread.
|
|
@@ -20,9 +21,21 @@ const DEFAULTS = {
|
|
|
20
21
|
|
|
21
22
|
function resolve(config, { rootDir } = {}) {
|
|
22
23
|
const jobs = { ...DEFAULTS, ...config.jobs };
|
|
23
|
-
if (jobs.dir && !path.isAbsolute(jobs.dir)) {
|
|
24
|
+
if (rootDir && jobs.dir && !path.isAbsolute(jobs.dir)) {
|
|
24
25
|
jobs.dir = path.resolve(rootDir, jobs.dir);
|
|
25
26
|
}
|
|
27
|
+
if (jobs.backoffMs !== undefined) {
|
|
28
|
+
jobs.backoffMs = normalizeDurationMs(jobs.backoffMs, 'jobs.backoffMs');
|
|
29
|
+
}
|
|
30
|
+
if (jobs.pollIntervalMs !== undefined) {
|
|
31
|
+
jobs.pollIntervalMs = normalizeDurationMs(jobs.pollIntervalMs, 'jobs.pollIntervalMs');
|
|
32
|
+
}
|
|
33
|
+
if (jobs.cooldownMs !== undefined) {
|
|
34
|
+
jobs.cooldownMs = normalizeDurationMs(jobs.cooldownMs, 'jobs.cooldownMs');
|
|
35
|
+
}
|
|
36
|
+
if (jobs.staleTimeoutMs !== undefined) {
|
|
37
|
+
jobs.staleTimeoutMs = normalizeDurationMs(jobs.staleTimeoutMs, 'jobs.staleTimeoutMs');
|
|
38
|
+
}
|
|
26
39
|
if (jobs.workerPoolSize === undefined) jobs.workerPoolSize = defaultWorkerPoolSize();
|
|
27
40
|
return { ...config, jobs };
|
|
28
41
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeDurationDays, normalizeDurationSeconds } from '../duration.js';
|
|
2
|
+
|
|
1
3
|
const DEFAULTS = {
|
|
2
4
|
driver: 'console',
|
|
3
5
|
enabled: true,
|
|
@@ -5,7 +7,20 @@ const DEFAULTS = {
|
|
|
5
7
|
|
|
6
8
|
function resolve(config) {
|
|
7
9
|
if (!config.mail) return config;
|
|
8
|
-
|
|
10
|
+
const mail = { ...DEFAULTS, ...config.mail };
|
|
11
|
+
if (mail.inbound?.imap?.pollIntervalSeconds !== undefined) {
|
|
12
|
+
mail.inbound.imap.pollIntervalSeconds = normalizeDurationSeconds(
|
|
13
|
+
mail.inbound.imap.pollIntervalSeconds,
|
|
14
|
+
'mail.inbound.imap.pollIntervalSeconds',
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
if (mail.inbound?.retentionDays !== undefined) {
|
|
18
|
+
mail.inbound.retentionDays = normalizeDurationDays(
|
|
19
|
+
mail.inbound.retentionDays,
|
|
20
|
+
'mail.inbound.retentionDays',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return { ...config, mail };
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
export default resolve;
|
|
@@ -3,7 +3,11 @@ const DEFAULTS = {
|
|
|
3
3
|
};
|
|
4
4
|
|
|
5
5
|
function resolve(config) {
|
|
6
|
-
|
|
6
|
+
const mcp = { ...DEFAULTS, ...config.mcp };
|
|
7
|
+
if (!mcp.secret && config.secrets?.values?.['mcp-api']) {
|
|
8
|
+
mcp.secret = config.secrets.values['mcp-api'];
|
|
9
|
+
}
|
|
10
|
+
return { ...config, mcp };
|
|
7
11
|
}
|
|
8
12
|
|
|
9
13
|
export default resolve;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
enabled: false,
|
|
5
|
+
dirs: ['plugins'],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function resolve(config, { rootDir } = {}) {
|
|
9
|
+
const plugins = { ...DEFAULTS, ...config.plugins };
|
|
10
|
+
const dirs = Array.isArray(plugins.dirs) ? plugins.dirs : [plugins.dirs];
|
|
11
|
+
plugins.dirs = dirs
|
|
12
|
+
.filter((dir) => typeof dir === 'string' && dir.length > 0)
|
|
13
|
+
.map((dir) => (rootDir && !path.isAbsolute(dir) ? path.resolve(rootDir, dir) : dir));
|
|
14
|
+
plugins.enabled = plugins.enabled === true;
|
|
15
|
+
return { ...config, plugins };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default resolve;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { normalizeDurationMs } from '../duration.js';
|
|
2
|
+
|
|
1
3
|
const DEFAULTS = {
|
|
2
4
|
tableName: 'arcway_queue',
|
|
3
5
|
lockCooldownMs: 5 * 60 * 1000,
|
|
4
6
|
};
|
|
5
7
|
|
|
6
8
|
function resolve(config) {
|
|
7
|
-
|
|
9
|
+
const queue = { ...DEFAULTS, ...config.queue };
|
|
10
|
+
if (queue.lockCooldownMs !== undefined) {
|
|
11
|
+
queue.lockCooldownMs = normalizeDurationMs(queue.lockCooldownMs, 'queue.lockCooldownMs');
|
|
12
|
+
}
|
|
13
|
+
return { ...config, queue };
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
export default resolve;
|