@oyolincc/create 0.0.0-dev-20260918103756 → 0.0.0-dev-20261006065712
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/package.json +1 -1
- package/templates/hono-fullstack/.vscode/launch.json +14 -3
- package/templates/hono-fullstack/.vscode/settings.json +3 -1
- package/templates/hono-fullstack/AGENTS.md +7 -0
- package/templates/hono-fullstack/README.md +28 -14
- package/templates/hono-fullstack/_gitignore +4 -0
- package/templates/hono-fullstack/apps/server/.dev.vars.example +9 -0
- package/templates/hono-fullstack/apps/server/package.json +17 -10
- package/templates/hono-fullstack/apps/server/src/adapters/cloudflare/entry.ts +36 -0
- package/templates/hono-fullstack/apps/server/src/adapters/nodejs/bootstrap.ts +3 -6
- package/templates/hono-fullstack/apps/server/src/adapters/nodejs/entry.ts +21 -6
- package/templates/hono-fullstack/apps/server/src/app.ts +4 -6
- package/templates/hono-fullstack/apps/server/src/config.ts +47 -0
- package/templates/hono-fullstack/apps/server/src/infra/logger/pino.ts +4 -0
- package/templates/hono-fullstack/apps/server/src/infra/work-runner/example/worker-entry.ts +4 -25
- package/templates/hono-fullstack/apps/server/src/infra/work-runner/pass-through/runner.ts +42 -0
- package/templates/{hono-server/apps/server/src/infra/work-runner/example → hono-fullstack/apps/server/src/infra/work-runner/piscina}/runner.ts +9 -9
- package/templates/hono-fullstack/apps/server/src/infra/work-runner/types.ts +7 -2
- package/templates/hono-fullstack/apps/server/src/infra/work-runner/utils/message.ts +7 -4
- package/templates/hono-fullstack/apps/server/src/modules/example/routes.ts +3 -5
- package/templates/hono-fullstack/apps/server/src/modules/example/work-task/tasks.test.ts +17 -5
- package/templates/hono-fullstack/apps/server/src/modules/example/work-task/tasks.ts +38 -15
- package/templates/hono-fullstack/apps/server/src/shared/app/types.ts +8 -4
- package/templates/hono-fullstack/apps/server/src/shared/network/validator.ts +10 -3
- package/templates/hono-fullstack/apps/server/tsconfig.json +1 -0
- package/templates/hono-fullstack/apps/server/vite.config.ts +8 -3
- package/templates/hono-fullstack/apps/server/wrangler.jsonc +29 -0
- package/templates/hono-fullstack/apps/stack/backend/cloudflare.ts +5 -0
- package/templates/hono-fullstack/apps/stack/backend/node.ts +3 -0
- package/templates/hono-fullstack/apps/stack/frontend/components/general/ErrorMessage.tsx +4 -0
- package/templates/hono-fullstack/apps/stack/frontend/components/layouts/AppLayout.tsx +11 -0
- package/templates/hono-fullstack/apps/stack/frontend/components/layouts/Header.tsx +7 -0
- package/templates/hono-fullstack/apps/stack/frontend/components/layouts/Page.tsx +5 -0
- package/templates/hono-fullstack/apps/stack/frontend/components/layouts/PageContainer.tsx +5 -0
- package/templates/hono-fullstack/apps/stack/frontend/modules/example/components/ExampleForm.tsx +45 -0
- package/templates/hono-fullstack/apps/stack/frontend/modules/example/components/ExampleList.tsx +13 -0
- package/templates/hono-fullstack/apps/stack/frontend/modules/example/{hooks.ts → hooks/api-hooks.ts} +2 -1
- package/templates/hono-fullstack/apps/stack/frontend/modules/example/stores/index.ts +3 -0
- package/templates/hono-fullstack/apps/stack/frontend/routeTree.gen.ts +21 -4
- package/templates/hono-fullstack/apps/stack/frontend/routes/api/$.ts +18 -0
- package/templates/hono-fullstack/apps/stack/frontend/routes/index.tsx +48 -2
- package/templates/hono-fullstack/apps/stack/frontend/vite-env.d.ts +5 -0
- package/templates/hono-fullstack/apps/stack/package.json +20 -4
- package/templates/hono-fullstack/apps/stack/tsconfig.json +7 -4
- package/templates/hono-fullstack/apps/stack/tsr.config.json +2 -2
- package/templates/hono-fullstack/apps/stack/vite.config.ts +75 -26
- package/templates/hono-fullstack/apps/stack/wrangler.jsonc +20 -0
- package/templates/hono-fullstack/envs/.env.example.server +1 -1
- package/templates/hono-fullstack/envs/.env.example.sqlite +1 -2
- package/templates/hono-fullstack/envs/.env.example.sqlite-d1 +3 -0
- package/templates/hono-fullstack/libs/db/drizzle-kit/sqlite-d1.config.ts +22 -0
- package/templates/hono-fullstack/libs/db/package.json +9 -6
- package/templates/hono-fullstack/libs/db/src/dialects/sqlite/d1-client.ts +12 -0
- package/templates/hono-fullstack/libs/db/src/dialects/sqlite/schema/columns.ts +5 -0
- package/templates/hono-fullstack/libs/db/src/dialects/sqlite/schema/modules/examples/examples.ts +2 -2
- package/templates/hono-fullstack/libs/db/vite.config.ts +1 -0
- package/templates/hono-fullstack/libs/definition/src/configs/defs/runtime.ts +1 -0
- package/templates/hono-fullstack/libs/definition/src/configs/defs/sqlite.ts +16 -3
- package/templates/hono-fullstack/oyo.config.json +2 -1
- package/templates/hono-fullstack/package.json +1 -5
- package/templates/hono-fullstack/pnpm-workspace.yaml +8 -6
- package/templates/hono-fullstack/turbo.json +26 -3
- package/templates/hono-fullstack/vite.config.ts +1 -0
- package/templates/hono-server/.vscode/launch.json +7 -3
- package/templates/hono-server/.vscode/settings.json +3 -1
- package/templates/hono-server/AGENTS.md +7 -0
- package/templates/hono-server/README.md +25 -12
- package/templates/hono-server/_gitignore +4 -0
- package/templates/hono-server/apps/server/.dev.vars.example +9 -0
- package/templates/hono-server/apps/server/package.json +18 -9
- package/templates/hono-server/apps/server/src/adapters/cloudflare/entry.ts +36 -0
- package/templates/hono-server/apps/server/src/adapters/nodejs/bootstrap.ts +3 -3
- package/templates/hono-server/apps/server/src/adapters/nodejs/entry.ts +28 -6
- package/templates/hono-server/apps/server/src/app.ts +4 -6
- package/templates/hono-server/apps/server/src/config.ts +47 -0
- package/templates/hono-server/apps/server/src/infra/logger/pino.ts +4 -0
- package/templates/hono-server/apps/server/src/infra/work-runner/example/worker-entry.ts +4 -25
- package/templates/hono-server/apps/server/src/infra/work-runner/pass-through/runner.ts +42 -0
- package/templates/{hono-fullstack/apps/server/src/infra/work-runner/example → hono-server/apps/server/src/infra/work-runner/piscina}/runner.ts +9 -9
- package/templates/hono-server/apps/server/src/infra/work-runner/types.ts +7 -2
- package/templates/hono-server/apps/server/src/infra/work-runner/utils/message.ts +7 -4
- package/templates/hono-server/apps/server/src/modules/example/contracts.ts +4 -0
- package/templates/hono-server/apps/server/src/modules/example/routes.ts +32 -5
- package/templates/hono-server/apps/server/src/modules/example/work-task/tasks.test.ts +17 -5
- package/templates/hono-server/apps/server/src/modules/example/work-task/tasks.ts +38 -15
- package/templates/hono-server/apps/server/src/shared/app/types.ts +8 -4
- package/templates/hono-server/apps/server/src/shared/network/validator.ts +10 -3
- package/templates/hono-server/apps/server/tsconfig.json +1 -0
- package/templates/hono-server/apps/server/vite.config.ts +8 -3
- package/templates/hono-server/apps/server/wrangler.jsonc +29 -0
- package/templates/hono-server/docker/server.Dockerfile +3 -5
- package/templates/hono-server/envs/.env.example.server +1 -1
- package/templates/hono-server/envs/.env.example.sqlite +1 -0
- package/templates/hono-server/envs/.env.example.sqlite-d1 +3 -0
- package/templates/hono-server/libs/db/drizzle-kit/sqlite-d1.config.ts +22 -0
- package/templates/hono-server/libs/db/drizzle-kit/sqlite.config.ts +22 -0
- package/templates/hono-server/libs/db/package.json +42 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/client.ts +35 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/d1-client.ts +12 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/columns.ts +37 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/index.ts +7 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/modules/examples/examples.ts +22 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/relations.ts +5 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/tables.ts +5 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/types.ts +7 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/schema/vo.ts +1 -0
- package/templates/hono-server/libs/db/src/dialects/sqlite/utils.ts +8 -0
- package/templates/hono-server/libs/db/src/shared/types.ts +26 -0
- package/templates/hono-server/libs/db/tsconfig.json +10 -0
- package/templates/hono-server/libs/db/vite.config.ts +22 -0
- package/templates/hono-server/libs/definition/src/configs/defs/runtime.ts +1 -0
- package/templates/hono-server/libs/definition/src/configs/defs/sqlite.ts +26 -0
- package/templates/hono-server/libs/definition/src/configs/index.ts +1 -0
- package/templates/hono-server/libs/definition/vite.config.ts +1 -1
- package/templates/hono-server/oyo.config.json +3 -1
- package/templates/hono-server/package.json +2 -3
- package/templates/hono-server/pnpm-workspace.yaml +13 -4
- package/templates/hono-server/turbo.json +26 -3
- package/templates/hono-server/vite.config.ts +1 -0
- package/templates/monorepo/.vscode/launch.json +0 -3
- package/templates/monorepo/.vscode/settings.json +3 -1
- package/templates/monorepo/AGENTS.md +7 -0
- package/templates/monorepo/_gitignore +4 -0
- package/templates/monorepo/pnpm-workspace.yaml +6 -3
- package/templates/monorepo/turbo.json +26 -3
- package/templates/monorepo/vite.config.ts +1 -0
- package/templates/hono-fullstack/apps/server/src/adapters/nodejs/config.ts +0 -53
- package/templates/hono-fullstack/apps/server/src/app.test.ts +0 -52
- package/templates/hono-fullstack/apps/server/src/infra/work-runner/example/runner.test.ts +0 -56
- package/templates/hono-fullstack/apps/stack/frontend/modules/example/ExamplePage.tsx +0 -57
- package/templates/hono-fullstack/apps/stack/nitro/handlers/hono.ts +0 -1
- package/templates/hono-fullstack/apps/stack/nitro/plugins/hono.ts +0 -9
- package/templates/hono-server/apps/server/src/adapters/nodejs/config.ts +0 -53
- package/templates/hono-server/apps/server/src/app.test.ts +0 -52
- package/templates/hono-server/apps/server/src/infra/work-runner/example/runner.test.ts +0 -56
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createD1Client } from '@repo/db/sqlite/d1-client'
|
|
2
|
+
import { env } from 'cloudflare:workers'
|
|
3
|
+
import { getRuntimeKey } from 'hono/adapter'
|
|
4
|
+
import { createApp } from '@/app'
|
|
5
|
+
import { resolveConfig } from '@/config'
|
|
6
|
+
import { setRootLogger } from '@/infra/logger/getter'
|
|
7
|
+
import { createPinoLogger } from '@/infra/logger/pino'
|
|
8
|
+
import { PassThroughWorkRunner } from '@/infra/work-runner/pass-through/runner'
|
|
9
|
+
import type { AppDependencies, AppLogger } from '@/shared/app/types'
|
|
10
|
+
|
|
11
|
+
const appConfig = resolveConfig(env as any)
|
|
12
|
+
const { runtime, log } = appConfig
|
|
13
|
+
const { rootLogger, loggerHandler } = createPinoLogger({
|
|
14
|
+
isLocal: runtime.isLocal,
|
|
15
|
+
logBase: log.base,
|
|
16
|
+
runtime: getRuntimeKey(),
|
|
17
|
+
logRoll: log.roll,
|
|
18
|
+
logPretty: log.pretty,
|
|
19
|
+
})
|
|
20
|
+
setRootLogger(rootLogger as unknown as AppLogger)
|
|
21
|
+
|
|
22
|
+
export const workRunner = new PassThroughWorkRunner()
|
|
23
|
+
const d1Client = createD1Client(env.DB)
|
|
24
|
+
const dependencies: AppDependencies = {
|
|
25
|
+
appConfig,
|
|
26
|
+
loggerHandler,
|
|
27
|
+
workRunner,
|
|
28
|
+
db: d1Client.db,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const app = createApp({
|
|
32
|
+
serverBasePath: appConfig.runtime.serverBasePath,
|
|
33
|
+
makeDependencies: () => dependencies,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export default app
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serve } from '@hono/node-server'
|
|
2
|
-
import app, {
|
|
2
|
+
import app, { closeNodeDependencies } from './entry'
|
|
3
3
|
|
|
4
4
|
function main() {
|
|
5
5
|
const port = Number(process.env.NODE_PORT)
|
|
@@ -38,8 +38,8 @@ function main() {
|
|
|
38
38
|
})
|
|
39
39
|
console.log('✔ HTTP server successfully closed.')
|
|
40
40
|
|
|
41
|
-
await
|
|
42
|
-
console.log('✔
|
|
41
|
+
await closeNodeDependencies()
|
|
42
|
+
console.log('✔ Node dependencies successfully closed.')
|
|
43
43
|
|
|
44
44
|
console.log('Graceful shutdown completed.')
|
|
45
45
|
process.exit(0)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { createSqliteClient } from '@repo/db/sqlite/sqlite'
|
|
2
|
+
import { sqliteSchemaConfig } from '@repo/definition/backend'
|
|
1
3
|
import { createApp } from '@/app'
|
|
2
|
-
import { resolveConfig } from '
|
|
4
|
+
import { resolveConfig } from '@/config'
|
|
3
5
|
import { createPinoLogger } from '@/infra/logger/pino'
|
|
4
6
|
import { getRuntimeKey } from 'hono/adapter'
|
|
5
7
|
import type { AppDependencies, AppLogger } from '@/shared/app/types'
|
|
6
|
-
import {
|
|
8
|
+
// import { PiscinaWorkRunner } from '@/infra/work-runner/piscina/runner'
|
|
9
|
+
import { PassThroughWorkRunner } from '@/infra/work-runner/pass-through/runner'
|
|
7
10
|
import { getLogger, setRootLogger } from '@/infra/logger/getter'
|
|
8
11
|
|
|
9
12
|
function installProcessErrorHandlers(isLocal: boolean) {
|
|
@@ -18,6 +21,10 @@ function installProcessErrorHandlers(isLocal: boolean) {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const appConfig = resolveConfig(process.env)
|
|
24
|
+
const sqliteConfig = sqliteSchemaConfig.resolve({
|
|
25
|
+
context: process.env,
|
|
26
|
+
matchContextKey: 'constantCase',
|
|
27
|
+
})
|
|
21
28
|
const { runtime, log } = appConfig
|
|
22
29
|
const { rootLogger, loggerHandler } = createPinoLogger({
|
|
23
30
|
isLocal: runtime.isLocal,
|
|
@@ -29,12 +36,27 @@ const { rootLogger, loggerHandler } = createPinoLogger({
|
|
|
29
36
|
setRootLogger(rootLogger as unknown as AppLogger)
|
|
30
37
|
installProcessErrorHandlers(runtime.isLocal)
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
// To enable Piscina, use PiscinaWorkRunner and enable the worker pack entry in vite.config.ts.
|
|
40
|
+
// export const workRunner = PiscinaWorkRunner.getInstance(appConfig.worker)
|
|
41
|
+
export const workRunner = new PassThroughWorkRunner()
|
|
42
|
+
export const sqliteClient = createSqliteClient(sqliteConfig.dbPath)
|
|
33
43
|
|
|
34
44
|
let dependencies: AppDependencies | undefined
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
let closePromise: Promise<void> | undefined
|
|
46
|
+
const app = createApp({
|
|
47
|
+
serverBasePath: appConfig.runtime.serverBasePath,
|
|
48
|
+
makeDependencies: () => {
|
|
49
|
+
dependencies ||= { appConfig, loggerHandler, workRunner, db: sqliteClient.db }
|
|
50
|
+
return dependencies
|
|
51
|
+
},
|
|
38
52
|
})
|
|
39
53
|
|
|
54
|
+
export const closeNodeDependencies = (): Promise<void> => {
|
|
55
|
+
closePromise ||= (async () => {
|
|
56
|
+
await workRunner.shutdown()
|
|
57
|
+
sqliteClient.close()
|
|
58
|
+
})()
|
|
59
|
+
return closePromise
|
|
60
|
+
}
|
|
61
|
+
|
|
40
62
|
export default app
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
2
|
import { contextStorage } from 'hono/context-storage'
|
|
3
3
|
import { csrf } from 'hono/csrf'
|
|
4
4
|
import { secureHeaders } from 'hono/secure-headers'
|
|
5
5
|
import { requestId } from 'hono/request-id'
|
|
6
6
|
import { corsMiddleware } from './middlewares/cors'
|
|
7
7
|
import { exampleRoutes } from './modules/example/routes'
|
|
8
|
-
import type {
|
|
8
|
+
import type { CreateAppOptions, AppEnv } from './shared/app/types'
|
|
9
9
|
import { E } from './shared/errors/defs'
|
|
10
10
|
import { globalErrorHandler } from './shared/errors/handler'
|
|
11
11
|
import { configureOpenAPI } from './shared/network/openapi'
|
|
12
12
|
import { objectEntries } from './shared/utils'
|
|
13
13
|
|
|
14
|
-
export const createApp = (
|
|
15
|
-
|
|
16
|
-
): Hono<AppEnv> => {
|
|
17
|
-
const app = new Hono<AppEnv>()
|
|
14
|
+
export const createApp = ({ serverBasePath, makeDependencies }: CreateAppOptions): Hono<AppEnv> => {
|
|
15
|
+
const app = new Hono<AppEnv>().basePath(serverBasePath)
|
|
18
16
|
|
|
19
17
|
app.use('*', async (c, next) => {
|
|
20
18
|
const deps = await makeDependencies(c)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
corsConfig,
|
|
3
|
+
logBaseConfig,
|
|
4
|
+
logPrettyRawConfig,
|
|
5
|
+
logRollRawConfig,
|
|
6
|
+
logRollSchemaConfig,
|
|
7
|
+
runtimeConfig,
|
|
8
|
+
workerConfig,
|
|
9
|
+
} from '@repo/definition/backend'
|
|
10
|
+
import { getRuntimeKey } from 'hono/adapter'
|
|
11
|
+
import { flatten as flattenValiIssues, isValiError } from 'valibot'
|
|
12
|
+
import type { AppConfig } from '@/shared/app/types'
|
|
13
|
+
|
|
14
|
+
export function resolveConfig(context: Record<string, unknown>): AppConfig {
|
|
15
|
+
try {
|
|
16
|
+
const fromEnv = { context, matchContextKey: 'constantCase' as const }
|
|
17
|
+
const runtime = runtimeConfig.resolve(fromEnv)
|
|
18
|
+
const runtimeKey = getRuntimeKey()
|
|
19
|
+
const isNodejsLike = runtimeKey === 'node' || runtimeKey === 'bun'
|
|
20
|
+
const log: AppConfig['log'] = { base: logBaseConfig.resolve(fromEnv) }
|
|
21
|
+
|
|
22
|
+
if (isNodejsLike) {
|
|
23
|
+
log.roll = {
|
|
24
|
+
...logRollSchemaConfig.resolve(fromEnv),
|
|
25
|
+
...logRollRawConfig.resolve(),
|
|
26
|
+
}
|
|
27
|
+
log.pretty = logPrettyRawConfig.resolve()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
runtime: { ...runtime, isLocal: runtime.serverEnv === 'local' },
|
|
32
|
+
log,
|
|
33
|
+
worker: workerConfig.resolve(fromEnv),
|
|
34
|
+
middleware: {
|
|
35
|
+
cors: corsConfig.resolve({
|
|
36
|
+
context: { domains: context.CORS_DOMAINS },
|
|
37
|
+
matchContextKey: 'camel',
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (isValiError(error)) {
|
|
43
|
+
throw new Error(`[resolveConfig] ${JSON.stringify(flattenValiIssues(error.issues))}`)
|
|
44
|
+
}
|
|
45
|
+
throw error
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -37,6 +37,10 @@ export const createPinoOptions = ({
|
|
|
37
37
|
}) => {
|
|
38
38
|
const pinoOptions: LoggerOptions = {
|
|
39
39
|
level: baseConfig.loggerLevel || (isLocal ? 'debug' : 'info'),
|
|
40
|
+
browser: {
|
|
41
|
+
asObject: true,
|
|
42
|
+
serialize: ['!stdSerializers.err', 'req', 'res', 'err'],
|
|
43
|
+
},
|
|
40
44
|
serializers: {
|
|
41
45
|
req: (req) => {
|
|
42
46
|
return {
|
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
/** WORKER 打包入口 */
|
|
2
|
-
import type { RunnerTaskInvocation
|
|
2
|
+
import type { RunnerTaskInvocation } from '../types'
|
|
3
3
|
import {
|
|
4
4
|
executeExampleTask as runExampleTask,
|
|
5
5
|
type ExampleRunnerTaskModel,
|
|
6
6
|
} from '@/modules/example/work-task/tasks'
|
|
7
|
-
import { isWorkerTaskBusinessError } from '../utils/business-error'
|
|
8
7
|
|
|
9
|
-
export
|
|
8
|
+
export function executeExampleTask({
|
|
10
9
|
input,
|
|
11
10
|
messagePort,
|
|
12
|
-
}: RunnerTaskInvocation<ExampleRunnerTaskModel['input']>):
|
|
13
|
-
|
|
14
|
-
> {
|
|
15
|
-
try {
|
|
16
|
-
const result = await runExampleTask(input, messagePort)
|
|
17
|
-
messagePort.postMessage({ type: 'result', data: result.data })
|
|
18
|
-
return result
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if (!isWorkerTaskBusinessError(error)) throw error
|
|
21
|
-
const result = {
|
|
22
|
-
ok: false,
|
|
23
|
-
error: {
|
|
24
|
-
code: 'EXAMPLE_PROCESS_FAILED' as const,
|
|
25
|
-
message: error.message,
|
|
26
|
-
...(error.details ? { details: error.details } : {}),
|
|
27
|
-
},
|
|
28
|
-
}
|
|
29
|
-
messagePort.postMessage({ type: 'error', error: result.error })
|
|
30
|
-
return result
|
|
31
|
-
} finally {
|
|
32
|
-
messagePort.close()
|
|
33
|
-
}
|
|
11
|
+
}: RunnerTaskInvocation<ExampleRunnerTaskModel['input']>): ReturnType<typeof runExampleTask> {
|
|
12
|
+
return runExampleTask(input, messagePort).finally(() => messagePort.close())
|
|
34
13
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { executeExampleTask } from '../example/worker-entry'
|
|
2
|
+
import { getLogger } from '@/infra/logger/getter'
|
|
3
|
+
import { WorkSubject } from '../constants'
|
|
4
|
+
import type {
|
|
5
|
+
AppWorkRunner,
|
|
6
|
+
RunnerTaskParams,
|
|
7
|
+
RunnerTaskRunOptions,
|
|
8
|
+
RunnerTaskRunResult,
|
|
9
|
+
WorkerMessage,
|
|
10
|
+
WorkerMessagePort,
|
|
11
|
+
} from '../types'
|
|
12
|
+
import { handleWorkerMessage } from '../utils/message'
|
|
13
|
+
|
|
14
|
+
function createMessagePort(subject: WorkSubject): WorkerMessagePort {
|
|
15
|
+
const logger = getLogger()
|
|
16
|
+
return {
|
|
17
|
+
postMessage(message: WorkerMessage) {
|
|
18
|
+
handleWorkerMessage(message, logger, { subject })
|
|
19
|
+
},
|
|
20
|
+
close() {},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PassThroughWorkRunner implements AppWorkRunner {
|
|
25
|
+
async run<TSubject extends WorkSubject>(
|
|
26
|
+
taskParams: RunnerTaskParams<TSubject>,
|
|
27
|
+
options?: RunnerTaskRunOptions,
|
|
28
|
+
): Promise<RunnerTaskRunResult<TSubject>> {
|
|
29
|
+
if (taskParams.subject !== WorkSubject.PROCESS_EXAMPLE) {
|
|
30
|
+
throw new Error(`[PassThroughWorkRunner] task name not found: ${String(taskParams.subject)}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
options?.signal?.throwIfAborted()
|
|
34
|
+
|
|
35
|
+
return executeExampleTask({
|
|
36
|
+
input: taskParams.input,
|
|
37
|
+
messagePort: createMessagePort(taskParams.subject),
|
|
38
|
+
}) as Promise<RunnerTaskRunResult<TSubject>>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async shutdown(): Promise<void> {}
|
|
42
|
+
}
|
|
@@ -14,21 +14,21 @@ import path from 'node:path'
|
|
|
14
14
|
import { MessageChannel, type MessagePort } from 'node:worker_threads'
|
|
15
15
|
import workersDir from 'virtual:workers'
|
|
16
16
|
import { getLogger } from '@/infra/logger/getter'
|
|
17
|
-
import type * as ExampleWorker from '
|
|
17
|
+
import type * as ExampleWorker from '../example/worker-entry'
|
|
18
18
|
import { handleWorkerMessage } from '../utils/message'
|
|
19
19
|
|
|
20
20
|
const taskNameMap = {
|
|
21
21
|
[WorkSubject.PROCESS_EXAMPLE]: 'executeExampleTask',
|
|
22
22
|
} satisfies Partial<Record<WorkSubject, keyof typeof ExampleWorker>>
|
|
23
23
|
|
|
24
|
-
export class
|
|
25
|
-
static #instance:
|
|
24
|
+
export class PiscinaWorkRunner implements AppWorkRunner {
|
|
25
|
+
static #instance: PiscinaWorkRunner | null = null
|
|
26
26
|
readonly #pool: Piscina
|
|
27
27
|
readonly #config: WorkerConfig
|
|
28
28
|
|
|
29
29
|
private constructor(config: WorkerConfig) {
|
|
30
30
|
if (!config) {
|
|
31
|
-
throw new Error('[
|
|
31
|
+
throw new Error('[PiscinaWorkRunner] config is required')
|
|
32
32
|
}
|
|
33
33
|
this.#pool = new Piscina({
|
|
34
34
|
filename: path.join(workersDir, 'example-worker.mjs'),
|
|
@@ -40,10 +40,10 @@ export class ExampleWorkRunner implements AppWorkRunner {
|
|
|
40
40
|
this.#config = config
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
public static getInstance(config: WorkerConfig):
|
|
43
|
+
public static getInstance(config: WorkerConfig): PiscinaWorkRunner {
|
|
44
44
|
if (!this.#instance) {
|
|
45
|
-
getLogger().info({ worker: config }, '
|
|
46
|
-
this.#instance = new
|
|
45
|
+
getLogger().info({ worker: config }, 'PiscinaWorkRunner initialized')
|
|
46
|
+
this.#instance = new PiscinaWorkRunner(config)
|
|
47
47
|
}
|
|
48
48
|
return this.#instance
|
|
49
49
|
}
|
|
@@ -54,7 +54,7 @@ export class ExampleWorkRunner implements AppWorkRunner {
|
|
|
54
54
|
): Promise<RunnerTaskRunResult<TSubject>> {
|
|
55
55
|
const targetName = taskNameMap[taskParams.subject]
|
|
56
56
|
if (!targetName) {
|
|
57
|
-
throw new Error(`[
|
|
57
|
+
throw new Error(`[PiscinaWorkRunner] task name not found: ${taskParams.subject}`)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
|
|
@@ -180,6 +180,6 @@ export class ExampleWorkRunner implements AppWorkRunner {
|
|
|
180
180
|
|
|
181
181
|
async shutdown(): Promise<void> {
|
|
182
182
|
await this.#pool.close()
|
|
183
|
-
if (
|
|
183
|
+
if (PiscinaWorkRunner.#instance === this) PiscinaWorkRunner.#instance = null
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExampleRunnerTaskModel } from '@/modules/example/work-task/tasks'
|
|
2
|
-
import type {
|
|
2
|
+
import type { Transferable } from 'node:worker_threads'
|
|
3
3
|
import type { WorkerErrorCode, WorkSubject } from './constants'
|
|
4
4
|
|
|
5
5
|
// export const WorkerInvocationType = Object.freeze({
|
|
@@ -43,7 +43,7 @@ export type RunnerTaskErrorCode<TSubject extends WorkSubject> =
|
|
|
43
43
|
|
|
44
44
|
export type RunnerTaskInvocation<TInput> = {
|
|
45
45
|
input: TInput
|
|
46
|
-
messagePort:
|
|
46
|
+
messagePort: WorkerMessagePort
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export type RunnerTaskJsonValue =
|
|
@@ -78,6 +78,11 @@ export type WorkerMessage =
|
|
|
78
78
|
error: RunnerTaskError
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
export interface WorkerMessagePort {
|
|
82
|
+
postMessage(message: WorkerMessage): void
|
|
83
|
+
close(): void
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
export type RunnerTaskError<TCode extends string = string> = {
|
|
82
87
|
code: TCode
|
|
83
88
|
/** 仅供服务端诊断,不应直接作为 HTTP 响应文案。 */
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { AppLogger } from '@/shared/app/types'
|
|
2
|
-
import type { RunnerTaskErrorDetails, WorkerMessage } from '../types'
|
|
3
|
-
import type { MessagePort } from 'node:worker_threads'
|
|
2
|
+
import type { RunnerTaskErrorDetails, WorkerMessage, WorkerMessagePort } from '../types'
|
|
4
3
|
import type { WorkSubject } from '../constants'
|
|
5
4
|
|
|
6
5
|
export function workerLog(
|
|
7
|
-
messagePort:
|
|
6
|
+
messagePort: WorkerMessagePort,
|
|
8
7
|
level: Extract<WorkerMessage, { type: 'log' }>['level'],
|
|
9
8
|
message: string,
|
|
10
9
|
data?: RunnerTaskErrorDetails,
|
|
@@ -12,7 +11,11 @@ export function workerLog(
|
|
|
12
11
|
messagePort.postMessage({ type: 'log', level, message, data } satisfies WorkerMessage)
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
export function workerProgress(
|
|
14
|
+
export function workerProgress(
|
|
15
|
+
messagePort: WorkerMessagePort,
|
|
16
|
+
value: number,
|
|
17
|
+
message?: string,
|
|
18
|
+
): void {
|
|
16
19
|
messagePort.postMessage({ type: 'progress', value, message } satisfies WorkerMessage)
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { Hono } from 'hono'
|
|
1
|
+
import { type Context, Hono } from 'hono'
|
|
2
2
|
import { describeRoute } from 'hono-openapi'
|
|
3
3
|
import * as v from 'valibot'
|
|
4
|
+
import { examples, vcExamples, type SqliteDBType } from '@repo/db/sqlite/schema'
|
|
4
5
|
import type { AppEnv } from '@/shared/app/types'
|
|
5
6
|
import { WorkSubject } from '@/infra/work-runner/constants'
|
|
6
7
|
import { E } from '@/shared/errors/defs'
|
|
7
8
|
import { jsonResponse, OpenApiTags } from '@/shared/network/openapi'
|
|
8
9
|
import { successJson } from '@/shared/network/utils'
|
|
9
10
|
import { generalValidator } from '@/shared/network/validator'
|
|
10
|
-
import { processExampleInputSchema } from './contracts'
|
|
11
|
+
import { createExampleInputSchema, processExampleInputSchema } from './contracts'
|
|
12
|
+
|
|
13
|
+
const getDb = (context: Context<AppEnv>) => context.get('db') as SqliteDBType
|
|
11
14
|
|
|
12
15
|
export const exampleRoutes = new Hono<AppEnv>()
|
|
13
16
|
.get(
|
|
@@ -20,6 +23,31 @@ export const exampleRoutes = new Hono<AppEnv>()
|
|
|
20
23
|
}),
|
|
21
24
|
(c) => successJson(c, { status: 'ok' }),
|
|
22
25
|
)
|
|
26
|
+
.get(
|
|
27
|
+
'/examples',
|
|
28
|
+
describeRoute({
|
|
29
|
+
operationId: 'listExamples',
|
|
30
|
+
tags: [OpenApiTags.Example],
|
|
31
|
+
summary: 'List example rows',
|
|
32
|
+
responses: { 200: { description: 'Example rows' } },
|
|
33
|
+
}),
|
|
34
|
+
async (c) => successJson(c, await getDb(c).select(vcExamples.default).from(examples)),
|
|
35
|
+
)
|
|
36
|
+
.post(
|
|
37
|
+
'/examples',
|
|
38
|
+
describeRoute({
|
|
39
|
+
operationId: 'createExample',
|
|
40
|
+
tags: [OpenApiTags.Example],
|
|
41
|
+
summary: 'Create an example row',
|
|
42
|
+
responses: { 200: { description: 'Created example row' } },
|
|
43
|
+
}),
|
|
44
|
+
generalValidator('json', createExampleInputSchema),
|
|
45
|
+
async (c) => {
|
|
46
|
+
const input = c.req.valid('json')
|
|
47
|
+
const [row] = await getDb(c).insert(examples).values(input).returning(vcExamples.default)
|
|
48
|
+
return successJson(c, row)
|
|
49
|
+
},
|
|
50
|
+
)
|
|
23
51
|
.post(
|
|
24
52
|
'/examples/process',
|
|
25
53
|
describeRoute({
|
|
@@ -30,10 +58,9 @@ export const exampleRoutes = new Hono<AppEnv>()
|
|
|
30
58
|
}),
|
|
31
59
|
generalValidator('json', processExampleInputSchema),
|
|
32
60
|
async (c) => {
|
|
33
|
-
const
|
|
34
|
-
const result = await c.get('exampleWorkRunner').run({
|
|
61
|
+
const result = await c.get('workRunner').run({
|
|
35
62
|
subject: WorkSubject.PROCESS_EXAMPLE,
|
|
36
|
-
input,
|
|
63
|
+
input: c.req.valid('json'),
|
|
37
64
|
})
|
|
38
65
|
if (!result.ok) {
|
|
39
66
|
throw E.NORMAL.INTERNAL.create({
|
|
@@ -18,19 +18,31 @@ describe('Example worker task', () => {
|
|
|
18
18
|
expect.arrayContaining([
|
|
19
19
|
expect.objectContaining({ type: 'log' }),
|
|
20
20
|
expect.objectContaining({ type: 'progress', value: 100 }),
|
|
21
|
+
expect.objectContaining({ type: 'result' }),
|
|
21
22
|
]),
|
|
22
23
|
)
|
|
23
24
|
port1.close()
|
|
24
25
|
port2.close()
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
it('
|
|
28
|
+
it('serializes a business exception and reports it', async () => {
|
|
28
29
|
const { port1, port2 } = new MessageChannel()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const messages: unknown[] = []
|
|
31
|
+
port1.on('message', (message) => messages.push(message))
|
|
32
|
+
const result = await executeExampleTask({ message: ' ' }, port2)
|
|
33
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
ok: false,
|
|
37
|
+
error: {
|
|
38
|
+
code: 'EXAMPLE_PROCESS_FAILED',
|
|
39
|
+
message: 'Message must not be empty',
|
|
40
|
+
details: { field: 'message' },
|
|
41
|
+
},
|
|
33
42
|
})
|
|
43
|
+
expect(messages).toEqual(
|
|
44
|
+
expect.arrayContaining([expect.objectContaining({ type: 'error' })]),
|
|
45
|
+
)
|
|
34
46
|
port1.close()
|
|
35
47
|
port2.close()
|
|
36
48
|
})
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { ProcessExampleInput, ProcessExampleOutput } from '@repo/definition/backend'
|
|
2
2
|
import { type WorkSubject } from '@/infra/work-runner/constants'
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
DefineRunnerTaskModel,
|
|
5
|
+
RunnerTaskResult,
|
|
6
|
+
WorkerMessagePort,
|
|
7
|
+
} from '@/infra/work-runner/types'
|
|
8
|
+
import {
|
|
9
|
+
isWorkerTaskBusinessError,
|
|
10
|
+
WorkerTaskBusinessError,
|
|
11
|
+
} from '@/infra/work-runner/utils/business-error'
|
|
5
12
|
import { workerLog, workerProgress } from '@/infra/work-runner/utils/message'
|
|
6
|
-
import type { MessagePort } from 'node:worker_threads'
|
|
7
13
|
|
|
8
14
|
export type ExampleTaskErrorCode = 'EXAMPLE_PROCESS_FAILED'
|
|
9
15
|
|
|
@@ -16,20 +22,37 @@ export type ExampleRunnerTaskModel = DefineRunnerTaskModel<
|
|
|
16
22
|
|
|
17
23
|
export async function executeExampleTask(
|
|
18
24
|
input: ExampleRunnerTaskModel['input'],
|
|
19
|
-
messagePort:
|
|
25
|
+
messagePort: WorkerMessagePort,
|
|
20
26
|
): Promise<RunnerTaskResult<ProcessExampleOutput, ExampleTaskErrorCode>> {
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
try {
|
|
28
|
+
workerLog(messagePort, 'debug', 'Example task started')
|
|
29
|
+
workerProgress(messagePort, 0, 'Validating input')
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
const original = input.message
|
|
32
|
+
const normalized = original.trim().replace(/\s+/g, ' ')
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
throw new WorkerTaskBusinessError('Message must not be empty', { field: 'message' })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
workerProgress(messagePort, 100, 'Example task completed')
|
|
38
|
+
const result = {
|
|
39
|
+
ok: true,
|
|
40
|
+
data: { original, normalized, length: normalized.length },
|
|
41
|
+
} as const
|
|
42
|
+
messagePort.postMessage({ type: 'result', data: result.data })
|
|
43
|
+
return result
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!isWorkerTaskBusinessError(error)) throw error
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
const result: RunnerTaskResult<ProcessExampleOutput, ExampleTaskErrorCode> = {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: {
|
|
50
|
+
code: 'EXAMPLE_PROCESS_FAILED',
|
|
51
|
+
message: error.message,
|
|
52
|
+
...(error.details ? { details: error.details } : {}),
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
messagePort.postMessage({ type: 'error', error: result.error })
|
|
56
|
+
return result
|
|
34
57
|
}
|
|
35
58
|
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
WorkerConfig,
|
|
9
9
|
} from '@repo/definition/backend'
|
|
10
10
|
import type { AppWorkRunner } from '@/infra/work-runner/types'
|
|
11
|
-
import type { MiddlewareHandler } from 'hono'
|
|
11
|
+
import type { MiddlewareHandler, Context } from 'hono'
|
|
12
12
|
import type { PinoLogger as HonoPinoLogger } from 'hono-pino'
|
|
13
13
|
import type { RequestIdVariables } from 'hono/request-id'
|
|
14
14
|
|
|
@@ -30,11 +30,15 @@ export type AppConfig = {
|
|
|
30
30
|
export type AppDependencies = {
|
|
31
31
|
appConfig: AppConfig
|
|
32
32
|
loggerHandler: MiddlewareHandler<{ Variables: { logger: AppLogger } }>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
db?: unknown
|
|
33
|
+
workRunner: AppWorkRunner
|
|
34
|
+
db: unknown
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
export type AppEnv = {
|
|
39
38
|
Variables: RequestIdVariables & AppDependencies & { logger?: AppLogger }
|
|
40
39
|
}
|
|
40
|
+
|
|
41
|
+
export type CreateAppOptions = {
|
|
42
|
+
serverBasePath: string
|
|
43
|
+
makeDependencies: (c: Context<AppEnv>) => Promise<AppDependencies> | AppDependencies
|
|
44
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ValidationTargets } from 'hono'
|
|
1
|
+
import type { Env, MiddlewareHandler, ValidationTargets } from 'hono'
|
|
2
2
|
import { validator } from 'hono-openapi'
|
|
3
|
-
import { flatten, type GenericSchema } from 'valibot'
|
|
3
|
+
import { flatten, type GenericSchema, type InferInput, type InferOutput } from 'valibot'
|
|
4
4
|
import { E } from '@/shared/errors/defs'
|
|
5
5
|
|
|
6
6
|
export const generalValidator = <
|
|
@@ -9,7 +9,14 @@ export const generalValidator = <
|
|
|
9
9
|
>(
|
|
10
10
|
target: TTarget,
|
|
11
11
|
schema: TSchema,
|
|
12
|
-
)
|
|
12
|
+
): MiddlewareHandler<
|
|
13
|
+
Env,
|
|
14
|
+
string,
|
|
15
|
+
{
|
|
16
|
+
in: { [K in TTarget]: InferInput<TSchema> }
|
|
17
|
+
out: { [K in TTarget]: InferOutput<TSchema> }
|
|
18
|
+
}
|
|
19
|
+
> =>
|
|
13
20
|
validator(target, schema, (result, _c) => {
|
|
14
21
|
if (!result.success) {
|
|
15
22
|
throw E.NORMAL.VALIDATION.create({
|
|
@@ -52,14 +52,19 @@ export default defineConfig(() => {
|
|
|
52
52
|
pack: {
|
|
53
53
|
...packConfig,
|
|
54
54
|
plugins: [virtualWorkers('pack')],
|
|
55
|
+
deps: {
|
|
56
|
+
neverBundle: ['cloudflare:workers'],
|
|
57
|
+
},
|
|
55
58
|
entry: {
|
|
56
59
|
...(isServe
|
|
57
60
|
? {}
|
|
58
61
|
: {
|
|
59
|
-
|
|
62
|
+
nodejsMain: './src/adapters/nodejs/bootstrap.ts',
|
|
60
63
|
}),
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
nodejsEntry: './src/adapters/nodejs/entry.ts',
|
|
65
|
+
cloudflareEntry: './src/adapters/cloudflare/entry.ts',
|
|
66
|
+
// Enable this entry together with PiscinaWorkRunner in the Node.js adapter.
|
|
67
|
+
// 'workers/example-worker': './src/infra/work-runner/example/worker-entry.ts',
|
|
63
68
|
},
|
|
64
69
|
// deps: {
|
|
65
70
|
// neverBundle: [...builtinModules],
|