@softize/opus 8.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/mcp — expõe as actions `ai:enabled` de um runtime como **tools MCP**, pra
|
|
3
|
+
* uma IA de FORA (Claude Desktop, o agente de um parceiro, um hub próprio) alcançar o app
|
|
4
|
+
* pelo protocolo. É o MESMO bridge do agente co-locado (`runtime.aiTools`) e a MESMA execução
|
|
5
|
+
* (`runtime.execute`, com o contexto resolvido) — o MCP é só o transporte.
|
|
6
|
+
*
|
|
7
|
+
* Você monta o transporte (stdio pra local, HTTP/SSE pra remoto) e conecta:
|
|
8
|
+
* `await createOpusMcpServer(runtime, { resolveContext }).connect(transport)`.
|
|
9
|
+
*
|
|
10
|
+
* `@modelcontextprotocol/sdk` já é dep do pacote.
|
|
11
|
+
*/
|
|
12
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
14
|
+
import { zodToJsonSchema } from 'zod-to-json-schema'
|
|
15
|
+
import type { ContextBase, Runtime } from '../core/index.ts'
|
|
16
|
+
|
|
17
|
+
export interface OpusMcpOptions {
|
|
18
|
+
name?: string
|
|
19
|
+
version?: string
|
|
20
|
+
/** Resolve a identidade da requisição MCP → contexto do runtime (auth). O `extra` traz o
|
|
21
|
+
* que o transporte capturou (headers/token). Sem isso, executa ANÔNIMO — o `can` nega, então
|
|
22
|
+
* só actions públicas rodam. Pra "como o usuário", valide o token (IdP) aqui e devolva o base. */
|
|
23
|
+
resolveContext?: (extra: unknown) => Promise<ContextBase> | ContextBase
|
|
24
|
+
/** Conversor Schema → JSON Schema (injetável). Default: `zod-to-json-schema` inline. */
|
|
25
|
+
toJsonSchema?: (schema: unknown) => unknown
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ANON: ContextBase = { user: null, tenantId: null, can: () => false, meta: {} }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Monta um `Server` MCP que expõe as actions `ai:enabled` do runtime como tools (ListTools) e
|
|
32
|
+
* as executa como o contexto resolvido (CallTool). Conecte num transporte com `.connect()`.
|
|
33
|
+
*/
|
|
34
|
+
export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {}): Server {
|
|
35
|
+
const toJsonSchema =
|
|
36
|
+
opts.toJsonSchema ?? ((schema: unknown) => zodToJsonSchema(schema as never, { $refStrategy: 'none' }))
|
|
37
|
+
|
|
38
|
+
const server = new Server(
|
|
39
|
+
{ name: opts.name ?? 'opus', version: opts.version ?? '0.0.0' },
|
|
40
|
+
{ capabilities: { tools: {} } },
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
44
|
+
tools: runtime.aiTools().map((t) => ({
|
|
45
|
+
name: t.name,
|
|
46
|
+
description: t.description,
|
|
47
|
+
inputSchema: toJsonSchema(t.inputSchema) as { type: 'object' },
|
|
48
|
+
})),
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
52
|
+
const base = opts.resolveContext ? await opts.resolveContext(extra) : ANON
|
|
53
|
+
const result = await runtime.execute(req.params.name, req.params.arguments ?? {}, base)
|
|
54
|
+
const payload = result.ok ? result.data : { error: result.error }
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text', text: JSON.stringify(payload ?? null) }],
|
|
57
|
+
isError: !result.ok,
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return server
|
|
62
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/queue/bullmq — BullMQ queue driver
|
|
3
|
+
*
|
|
4
|
+
* Implementa `QueueAdapter` wrappando uma instância `Queue` do BullMQ.
|
|
5
|
+
* Faz mapping `JobSpec` ↔ BullMQ job e `BullMQ state` → `JobStatus`.
|
|
6
|
+
*
|
|
7
|
+
* Worker (consumer-side) é responsabilidade do consumer — esse adapter
|
|
8
|
+
* só **enfileira** e **consulta**. Pra processar, o consumer registra
|
|
9
|
+
* um `new Worker(queueName, processor, { connection })` que chama
|
|
10
|
+
* `runtime.execute()` no callback.
|
|
11
|
+
*
|
|
12
|
+
* Uso:
|
|
13
|
+
* import { Queue } from 'bullmq'
|
|
14
|
+
* import { bullmqQueue } from '@softize/opus/queue/bullmq'
|
|
15
|
+
*
|
|
16
|
+
* const queue = new Queue('tbdlib-jobs', { connection: redisOpts })
|
|
17
|
+
* createRuntime({ queue: bullmqQueue({ queue }) })
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
JobHandle,
|
|
22
|
+
JobSpec,
|
|
23
|
+
JobStatus,
|
|
24
|
+
QueueAdapter,
|
|
25
|
+
} from '../../core/index.ts'
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// BullMQ-compatible interfaces (zero direct dep no tipo público)
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
/** Estado nativo do BullMQ retornado por `job.getState()`. */
|
|
32
|
+
export type BullMQState =
|
|
33
|
+
| 'waiting'
|
|
34
|
+
| 'waiting-children'
|
|
35
|
+
| 'active'
|
|
36
|
+
| 'completed'
|
|
37
|
+
| 'failed'
|
|
38
|
+
| 'delayed'
|
|
39
|
+
| 'paused'
|
|
40
|
+
| 'prioritized'
|
|
41
|
+
| 'unknown'
|
|
42
|
+
|
|
43
|
+
export interface BullMQJob {
|
|
44
|
+
id?: string | undefined
|
|
45
|
+
data: unknown
|
|
46
|
+
returnvalue?: unknown
|
|
47
|
+
failedReason?: string | undefined
|
|
48
|
+
attemptsMade: number
|
|
49
|
+
timestamp: number
|
|
50
|
+
processedOn?: number | undefined
|
|
51
|
+
finishedOn?: number | undefined
|
|
52
|
+
progress: number | object
|
|
53
|
+
getState(): Promise<BullMQState>
|
|
54
|
+
remove(): Promise<void>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BullMQQueue {
|
|
58
|
+
add(
|
|
59
|
+
name: string,
|
|
60
|
+
data: unknown,
|
|
61
|
+
opts?: { jobId?: string },
|
|
62
|
+
): Promise<BullMQJob>
|
|
63
|
+
getJob(jobId: string): Promise<BullMQJob | undefined | null>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Adapter factory
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
export interface BullmqQueueOptions {
|
|
71
|
+
queue: BullMQQueue
|
|
72
|
+
name?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function bullmqQueue(options: BullmqQueueOptions): QueueAdapter {
|
|
76
|
+
const { queue, name = 'bullmq' } = options
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
kind: 'queue',
|
|
81
|
+
|
|
82
|
+
async enqueue(spec: JobSpec): Promise<JobHandle> {
|
|
83
|
+
const job = await queue.add(spec.action, spec, { jobId: spec.jobId })
|
|
84
|
+
return jobToHandle(spec.action, job, 'queued')
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async status(jobId: string): Promise<JobHandle | null> {
|
|
88
|
+
const job = await queue.getJob(jobId)
|
|
89
|
+
if (job === undefined || job === null) return null
|
|
90
|
+
const state = await job.getState()
|
|
91
|
+
const action = readAction(job)
|
|
92
|
+
return jobToHandle(action, job, mapState(state))
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async cancel(jobId: string): Promise<boolean> {
|
|
96
|
+
const job = await queue.getJob(jobId)
|
|
97
|
+
if (job === undefined || job === null) return false
|
|
98
|
+
await job.remove()
|
|
99
|
+
return true
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Mapping
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* BullMQ state → tbdlib JobStatus.
|
|
110
|
+
*
|
|
111
|
+
* tbdlib não tem 'paused' nem 'delayed' separados — todos viram 'queued'.
|
|
112
|
+
* Distinção fina fica no `details.meta.bullmqState` se consumer precisar.
|
|
113
|
+
*/
|
|
114
|
+
export function mapState(state: BullMQState): JobStatus {
|
|
115
|
+
switch (state) {
|
|
116
|
+
case 'completed':
|
|
117
|
+
return 'done'
|
|
118
|
+
case 'failed':
|
|
119
|
+
return 'failed'
|
|
120
|
+
case 'active':
|
|
121
|
+
return 'running'
|
|
122
|
+
case 'waiting':
|
|
123
|
+
case 'waiting-children':
|
|
124
|
+
case 'delayed':
|
|
125
|
+
case 'paused':
|
|
126
|
+
case 'prioritized':
|
|
127
|
+
return 'queued'
|
|
128
|
+
/* v8 ignore next 3 — 'unknown' é defensivo; BullMQ documenta os estados acima */
|
|
129
|
+
case 'unknown':
|
|
130
|
+
default:
|
|
131
|
+
return 'queued'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function jobToHandle(
|
|
136
|
+
action: string,
|
|
137
|
+
job: BullMQJob,
|
|
138
|
+
status: JobStatus,
|
|
139
|
+
): JobHandle {
|
|
140
|
+
const handle: JobHandle = {
|
|
141
|
+
jobId: job.id ?? '',
|
|
142
|
+
action,
|
|
143
|
+
status,
|
|
144
|
+
enqueuedAt: toIso(job.timestamp),
|
|
145
|
+
attempts: job.attemptsMade,
|
|
146
|
+
}
|
|
147
|
+
if (job.processedOn !== undefined) handle.startedAt = toIso(job.processedOn)
|
|
148
|
+
if (job.finishedOn !== undefined) handle.finishedAt = toIso(job.finishedOn)
|
|
149
|
+
if (status === 'done' && job.returnvalue !== undefined) handle.data = job.returnvalue
|
|
150
|
+
if (status === 'failed' && job.failedReason !== undefined) {
|
|
151
|
+
handle.error = {
|
|
152
|
+
code: 'queue.job_failed',
|
|
153
|
+
category: 'internal',
|
|
154
|
+
message: job.failedReason,
|
|
155
|
+
severity: 'error',
|
|
156
|
+
retriable: false,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const progress = readProgress(job)
|
|
160
|
+
if (progress !== undefined) handle.progress = progress
|
|
161
|
+
return handle
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readAction(job: BullMQJob): string {
|
|
165
|
+
if (
|
|
166
|
+
typeof job.data === 'object' &&
|
|
167
|
+
job.data !== null &&
|
|
168
|
+
'action' in job.data &&
|
|
169
|
+
typeof (job.data as { action: unknown }).action === 'string'
|
|
170
|
+
) {
|
|
171
|
+
return (job.data as { action: string }).action
|
|
172
|
+
}
|
|
173
|
+
return ''
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readProgress(job: BullMQJob): JobHandle['progress'] | undefined {
|
|
177
|
+
const p = job.progress
|
|
178
|
+
if (typeof p === 'number') {
|
|
179
|
+
if (p === 0) return undefined
|
|
180
|
+
return { percent: p }
|
|
181
|
+
}
|
|
182
|
+
if (typeof p === 'object' && p !== null && Object.keys(p).length > 0) {
|
|
183
|
+
return p as JobHandle['progress']
|
|
184
|
+
}
|
|
185
|
+
return undefined
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toIso(ms: number): string {
|
|
189
|
+
return new Date(ms).toISOString()
|
|
190
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/queue — shared helpers/types for queue drivers.
|
|
3
|
+
*
|
|
4
|
+
* Drivers (bullmq, futuro: inngest, trigger-dev) implementam QueueAdapter
|
|
5
|
+
* do core. Espaço reservado pra helpers comuns (job status mapping,
|
|
6
|
+
* retry calculator universal).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/scheduler/node-cron — in-process cron scheduler driver.
|
|
3
|
+
*
|
|
4
|
+
* Wrappa `node-cron` implementando `SchedulerAdapter`. Roda na mesma
|
|
5
|
+
* process; suficiente pra monolitos. Pra ambientes distribuídos (múltiplos
|
|
6
|
+
* pods rodando o mesmo schedule), use o driver `/bullmq` (a vir) ou
|
|
7
|
+
* `/temporal` que coordenam via Redis/storage.
|
|
8
|
+
*
|
|
9
|
+
* Uso:
|
|
10
|
+
* import { nodeCronScheduler } from '@softize/opus/scheduler/node-cron'
|
|
11
|
+
*
|
|
12
|
+
* createRuntime({
|
|
13
|
+
* scheduler: nodeCronScheduler(),
|
|
14
|
+
* // ...
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { schedule as cronSchedule, type ScheduledTask } from 'node-cron'
|
|
19
|
+
import type {
|
|
20
|
+
ScheduleDef,
|
|
21
|
+
SchedulerAdapter,
|
|
22
|
+
Unsubscribe,
|
|
23
|
+
} from '../../core/index.ts'
|
|
24
|
+
import { everyToCron } from '../every.ts'
|
|
25
|
+
|
|
26
|
+
export interface NodeCronOptions {
|
|
27
|
+
/** Nome do adapter. Default 'node-cron'. */
|
|
28
|
+
name?: string
|
|
29
|
+
|
|
30
|
+
/** Override do logger de erros. Default console.error. */
|
|
31
|
+
onError?: (err: unknown, schedule: ScheduleDef) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function nodeCronScheduler(
|
|
35
|
+
options: NodeCronOptions = {},
|
|
36
|
+
): SchedulerAdapter {
|
|
37
|
+
const { name = 'node-cron', onError = defaultOnError } = options
|
|
38
|
+
const tasks: ScheduledTask[] = []
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
kind: 'scheduler',
|
|
43
|
+
|
|
44
|
+
register(spec, fire): Unsubscribe {
|
|
45
|
+
const cron = resolveCron(spec)
|
|
46
|
+
const cronOptions: { scheduled: true; timezone?: string } = {
|
|
47
|
+
scheduled: true,
|
|
48
|
+
}
|
|
49
|
+
if (spec.timezone !== undefined) cronOptions.timezone = spec.timezone
|
|
50
|
+
const task = cronSchedule(
|
|
51
|
+
cron,
|
|
52
|
+
() => {
|
|
53
|
+
// Wrap sync throws + async rejects no mesmo handler
|
|
54
|
+
void (async () => {
|
|
55
|
+
try {
|
|
56
|
+
await fire()
|
|
57
|
+
} catch (err) {
|
|
58
|
+
onError(err, spec)
|
|
59
|
+
}
|
|
60
|
+
})()
|
|
61
|
+
},
|
|
62
|
+
cronOptions,
|
|
63
|
+
)
|
|
64
|
+
tasks.push(task)
|
|
65
|
+
return () => {
|
|
66
|
+
task.stop()
|
|
67
|
+
const idx = tasks.indexOf(task)
|
|
68
|
+
if (idx >= 0) tasks.splice(idx, 1)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async dispose() {
|
|
73
|
+
for (const task of tasks.splice(0)) task.stop()
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async healthCheck() {
|
|
77
|
+
return { ok: true, details: { tasks: tasks.length } }
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveCron(spec: ScheduleDef): string {
|
|
83
|
+
if (spec.cron !== undefined) return spec.cron
|
|
84
|
+
if (spec.every !== undefined) return everyToCron(spec.every)
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Schedule "${spec.name}" must declare either 'cron' or 'every'.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function defaultOnError(err: unknown, schedule: ScheduleDef): void {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.error(`[tbdlib/scheduler] schedule "${schedule.name}" fire failed:`, err)
|
|
93
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversão de `every` shorthand → expressão cron de 5 campos.
|
|
3
|
+
*
|
|
4
|
+
* Suporta unidades: 's' (segundo), 'm' (minuto), 'h' (hora).
|
|
5
|
+
* - '15s' → '*\/15 * * * * *' (6-field cron com segundos)
|
|
6
|
+
* - '5m' → '*\/5 * * * *'
|
|
7
|
+
* - '1h' → '0 * * * *' (toda hora cheia)
|
|
8
|
+
* - '6h' → '0 *\/6 * * *'
|
|
9
|
+
* - '1d' → '0 0 * * *' (meia-noite)
|
|
10
|
+
*
|
|
11
|
+
* Não há "every 3 days" — limita ao que faz sentido em cron.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function everyToCron(every: string): string {
|
|
15
|
+
const match = /^(\d+)(s|m|h|d)$/.exec(every.trim())
|
|
16
|
+
if (match === null) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Invalid 'every' shorthand: "${every}". ` +
|
|
19
|
+
`Expected formato "<n><s|m|h|d>" (ex: "15s", "5m", "1h", "1d").`,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
const n = Number.parseInt(match[1] as string, 10)
|
|
23
|
+
const unit = match[2] as 's' | 'm' | 'h' | 'd'
|
|
24
|
+
|
|
25
|
+
if (n <= 0) {
|
|
26
|
+
throw new Error(`'every' value must be > 0, got "${every}".`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
switch (unit) {
|
|
30
|
+
case 's':
|
|
31
|
+
if (n > 59) throw new Error(`'every' seconds must be <= 59, got ${n}.`)
|
|
32
|
+
return `*/${n} * * * * *`
|
|
33
|
+
case 'm':
|
|
34
|
+
if (n > 59) throw new Error(`'every' minutes must be <= 59, got ${n}.`)
|
|
35
|
+
return `*/${n} * * * *`
|
|
36
|
+
case 'h':
|
|
37
|
+
if (n > 23) throw new Error(`'every' hours must be <= 23, got ${n}.`)
|
|
38
|
+
return n === 1 ? '0 * * * *' : `0 */${n} * * *`
|
|
39
|
+
case 'd':
|
|
40
|
+
if (n !== 1) {
|
|
41
|
+
throw new Error(`'every' days only supports 1d. For longer intervals use cron.`)
|
|
42
|
+
}
|
|
43
|
+
return '0 0 * * *'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/scheduler — shared helpers/types for scheduler drivers.
|
|
3
|
+
*
|
|
4
|
+
* Drivers (node-cron, futuro: bullmq-repeatable, temporal) implementam
|
|
5
|
+
* SchedulerAdapter do core. Conversões cron ↔ shorthand `every` moram
|
|
6
|
+
* aqui pra serem reusadas entre drivers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { everyToCron } from './every.ts'
|