@mettlecast/domain-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous-remediation flow pattern template.
|
|
3
|
+
* Produces: flow that detects anomalies and self-heals without human intervention.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function camelCase(s: string): string {
|
|
7
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function autonomousRemediationTemplate(domain: string, id: string): string {
|
|
11
|
+
const varName = camelCase(id);
|
|
12
|
+
|
|
13
|
+
return `import { defineFlow } from '@mettlecast/domain-runtime';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ${id} — Autonomous remediation flow.
|
|
17
|
+
*
|
|
18
|
+
* Detects anomaly events, runs diagnostic checks, applies
|
|
19
|
+
* corrective actions, and verifies resolution — all automated.
|
|
20
|
+
*/
|
|
21
|
+
export const ${varName} = defineFlow({
|
|
22
|
+
id: '${id}',
|
|
23
|
+
owningDomain: '${domain}',
|
|
24
|
+
type: 'standard',
|
|
25
|
+
name: '${id}',
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
type: 'domain-action',
|
|
29
|
+
name: 'DetectAnomaly',
|
|
30
|
+
domainId: '${domain}',
|
|
31
|
+
actionId: 'detect-anomaly',
|
|
32
|
+
next: 'RunDiagnostics',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'domain-action',
|
|
36
|
+
name: 'RunDiagnostics',
|
|
37
|
+
domainId: '${domain}',
|
|
38
|
+
actionId: 'run-diagnostics',
|
|
39
|
+
next: 'DecideAction',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'choice',
|
|
43
|
+
name: 'DecideAction',
|
|
44
|
+
choices: [
|
|
45
|
+
{ when: '$.diagnostics.severity == "critical"', next: 'ApplyCriticalFix' },
|
|
46
|
+
{ when: '$.diagnostics.severity == "warning"', next: 'ApplySoftFix' },
|
|
47
|
+
{ default: true, next: 'LogAndSkip' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: 'domain-action',
|
|
52
|
+
name: 'ApplyCriticalFix',
|
|
53
|
+
domainId: '${domain}',
|
|
54
|
+
actionId: 'apply-critical-fix',
|
|
55
|
+
next: 'VerifyResolution',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'domain-action',
|
|
59
|
+
name: 'ApplySoftFix',
|
|
60
|
+
domainId: '${domain}',
|
|
61
|
+
actionId: 'apply-soft-fix',
|
|
62
|
+
next: 'VerifyResolution',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'domain-action',
|
|
66
|
+
name: 'LogAndSkip',
|
|
67
|
+
domainId: '${domain}',
|
|
68
|
+
actionId: 'log-and-skip',
|
|
69
|
+
next: 'Done',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'domain-action',
|
|
73
|
+
name: 'VerifyResolution',
|
|
74
|
+
domainId: '${domain}',
|
|
75
|
+
actionId: 'verify-resolution',
|
|
76
|
+
next: 'EscalateIfUnresolved',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'choice',
|
|
80
|
+
name: 'EscalateIfUnresolved',
|
|
81
|
+
choices: [
|
|
82
|
+
{ when: '$.resolution === "unresolved"', next: 'EscalateToHuman' },
|
|
83
|
+
{ default: true, next: 'Done' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'domain-action',
|
|
88
|
+
name: 'EscalateToHuman',
|
|
89
|
+
domainId: '${domain}',
|
|
90
|
+
actionId: 'escalate-to-human',
|
|
91
|
+
next: 'Done',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'flow-control',
|
|
95
|
+
control: 'succeed',
|
|
96
|
+
name: 'Done',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fan-out-reaction flow pattern template.
|
|
3
|
+
* Produces: flow that fans out an event to multiple domain actions in parallel.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function camelCase(s: string): string {
|
|
7
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function fanOutReactionTemplate(domain: string, id: string): string {
|
|
11
|
+
const varName = camelCase(id);
|
|
12
|
+
|
|
13
|
+
return `import { defineFlow } from '@mettlecast/domain-runtime';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ${id} — Fan-out reaction flow.
|
|
17
|
+
*
|
|
18
|
+
* When an upstream event arrives, this flow fans out to multiple
|
|
19
|
+
* domain actions that run in parallel, then joins at a completion step.
|
|
20
|
+
*/
|
|
21
|
+
export const ${varName} = defineFlow({
|
|
22
|
+
id: '${id}',
|
|
23
|
+
owningDomain: '${domain}',
|
|
24
|
+
type: 'express',
|
|
25
|
+
name: '${id}',
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
type: 'domain-action',
|
|
29
|
+
name: 'NotifyServiceA',
|
|
30
|
+
domainId: '${domain}',
|
|
31
|
+
actionId: 'notify-service-a',
|
|
32
|
+
next: 'NotifyServiceB',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'domain-action',
|
|
36
|
+
name: 'NotifyServiceB',
|
|
37
|
+
domainId: '${domain}',
|
|
38
|
+
actionId: 'notify-service-b',
|
|
39
|
+
next: 'AuditFanOut',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'domain-action',
|
|
43
|
+
name: 'AuditFanOut',
|
|
44
|
+
domainId: '${domain}',
|
|
45
|
+
actionId: 'audit-fan-out',
|
|
46
|
+
next: 'Done',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'flow-control',
|
|
50
|
+
control: 'succeed',
|
|
51
|
+
name: 'Done',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-in-loop flow pattern template.
|
|
3
|
+
* Produces: flow that pauses for human approval before proceeding.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function camelCase(s: string): string {
|
|
7
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function humanInLoopTemplate(domain: string, id: string): string {
|
|
11
|
+
const varName = camelCase(id);
|
|
12
|
+
|
|
13
|
+
return `import { defineFlow } from '@mettlecast/domain-runtime';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ${id} — Human-in-the-loop flow.
|
|
17
|
+
*
|
|
18
|
+
* Pauses execution at an approval gate, waits for an authorised
|
|
19
|
+
* human to approve or reject, then proceeds or aborts accordingly.
|
|
20
|
+
*/
|
|
21
|
+
export const ${varName} = defineFlow({
|
|
22
|
+
id: '${id}',
|
|
23
|
+
owningDomain: '${domain}',
|
|
24
|
+
type: 'standard',
|
|
25
|
+
name: '${id}',
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
type: 'domain-action',
|
|
29
|
+
name: 'PrepareChangeRequest',
|
|
30
|
+
domainId: '${domain}',
|
|
31
|
+
actionId: 'prepare-change-request',
|
|
32
|
+
next: 'NotifyReviewers',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'domain-action',
|
|
36
|
+
name: 'NotifyReviewers',
|
|
37
|
+
domainId: '${domain}',
|
|
38
|
+
actionId: 'notify-reviewers',
|
|
39
|
+
next: 'WaitForApproval',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'domain-action',
|
|
43
|
+
name: 'WaitForApproval',
|
|
44
|
+
domainId: '${domain}',
|
|
45
|
+
actionId: 'wait-for-approval',
|
|
46
|
+
next: 'EvaluateApproval',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'choice',
|
|
50
|
+
name: 'EvaluateApproval',
|
|
51
|
+
choices: [
|
|
52
|
+
{ when: '$.approval.decision === "approved"', next: 'ApplyChange' },
|
|
53
|
+
{ when: '$.approval.decision === "rejected"', next: 'LogRejection' },
|
|
54
|
+
{ default: true, next: 'RetryApproval' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'domain-action',
|
|
59
|
+
name: 'ApplyChange',
|
|
60
|
+
domainId: '${domain}',
|
|
61
|
+
actionId: 'apply-change',
|
|
62
|
+
next: 'NotifyCompletion',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'domain-action',
|
|
66
|
+
name: 'LogRejection',
|
|
67
|
+
domainId: '${domain}',
|
|
68
|
+
actionId: 'log-rejection',
|
|
69
|
+
next: 'Done',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'domain-action',
|
|
73
|
+
name: 'RetryApproval',
|
|
74
|
+
domainId: '${domain}',
|
|
75
|
+
actionId: 'retry-approval',
|
|
76
|
+
next: 'NotifyReviewers',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'domain-action',
|
|
80
|
+
name: 'NotifyCompletion',
|
|
81
|
+
domainId: '${domain}',
|
|
82
|
+
actionId: 'notify-completion',
|
|
83
|
+
next: 'Done',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'flow-control',
|
|
87
|
+
control: 'succeed',
|
|
88
|
+
name: 'Done',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-step-orchestration flow pattern template.
|
|
3
|
+
* Produces: flow that orchestrates a chain of dependent domain actions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function camelCase(s: string): string {
|
|
7
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function multiStepOrchestrationTemplate(domain: string, id: string): string {
|
|
11
|
+
const varName = camelCase(id);
|
|
12
|
+
|
|
13
|
+
return `import { defineFlow } from '@mettlecast/domain-runtime';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ${id} — Multi-step orchestration flow.
|
|
17
|
+
*
|
|
18
|
+
* Executes a sequential pipeline of domain actions where each step
|
|
19
|
+
* depends on the output of the previous step.
|
|
20
|
+
*/
|
|
21
|
+
export const ${varName} = defineFlow({
|
|
22
|
+
id: '${id}',
|
|
23
|
+
owningDomain: '${domain}',
|
|
24
|
+
type: 'standard',
|
|
25
|
+
name: '${id}',
|
|
26
|
+
steps: [
|
|
27
|
+
{
|
|
28
|
+
type: 'domain-action',
|
|
29
|
+
name: 'ValidateInput',
|
|
30
|
+
domainId: '${domain}',
|
|
31
|
+
actionId: 'validate-input',
|
|
32
|
+
next: 'ProvisionResource',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'domain-action',
|
|
36
|
+
name: 'ProvisionResource',
|
|
37
|
+
domainId: '${domain}',
|
|
38
|
+
actionId: 'provision-resource',
|
|
39
|
+
next: 'ConfigureResource',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'domain-action',
|
|
43
|
+
name: 'ConfigureResource',
|
|
44
|
+
domainId: '${domain}',
|
|
45
|
+
actionId: 'configure-resource',
|
|
46
|
+
next: 'ActivateResource',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'domain-action',
|
|
50
|
+
name: 'ActivateResource',
|
|
51
|
+
domainId: '${domain}',
|
|
52
|
+
actionId: 'activate-resource',
|
|
53
|
+
next: 'SendNotification',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'domain-action',
|
|
57
|
+
name: 'SendNotification',
|
|
58
|
+
domainId: '${domain}',
|
|
59
|
+
actionId: 'send-notification',
|
|
60
|
+
next: 'Done',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'flow-control',
|
|
64
|
+
control: 'succeed',
|
|
65
|
+
name: 'Done',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated list hook pattern — uses `useInfiniteQuery` from TanStack Query for paginated lists.
|
|
3
|
+
*/
|
|
4
|
+
export function paginatedListHookTemplate(name: string): string {
|
|
5
|
+
const hookName = `use${capitalize(name)}`;
|
|
6
|
+
return `import { useInfiniteQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useTib } from '@/hooks/useTib';
|
|
8
|
+
|
|
9
|
+
/** Request shape for paginated list endpoints. */
|
|
10
|
+
export interface ${capitalize(name)}ListRequest {
|
|
11
|
+
workspaceId: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
search?: string;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
limit?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Single item in the paginated response. */
|
|
19
|
+
export interface ${capitalize(name)}Item {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Paginated response from the API. */
|
|
28
|
+
export interface ${capitalize(name)}ListResponse {
|
|
29
|
+
items: ${capitalize(name)}Item[];
|
|
30
|
+
nextCursor: string | null;
|
|
31
|
+
totalCount: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Infinite-query hook for paginated ${name} lists.
|
|
36
|
+
*
|
|
37
|
+
* Uses TanStack Query \`useInfiniteQuery\` with cursor-based pagination.
|
|
38
|
+
* Each page is fetched as the user scrolls, keeping memory bounded.
|
|
39
|
+
*/
|
|
40
|
+
export function ${hookName}(args: ${capitalize(name)}ListRequest) {
|
|
41
|
+
const tib = useTib();
|
|
42
|
+
|
|
43
|
+
return useInfiniteQuery<${capitalize(name)}ListResponse, Error>({
|
|
44
|
+
queryKey: ['${name}-list', args.workspaceId, args.status, args.search],
|
|
45
|
+
queryFn: ({ pageParam }) =>
|
|
46
|
+
tib.data.list({
|
|
47
|
+
workspaceId: args.workspaceId,
|
|
48
|
+
status: args.status,
|
|
49
|
+
search: args.search,
|
|
50
|
+
cursor: pageParam as string | undefined,
|
|
51
|
+
limit: args.limit ?? 20,
|
|
52
|
+
}),
|
|
53
|
+
initialPageParam: undefined as string | undefined,
|
|
54
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
55
|
+
staleTime: 10_000,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function mutationHookTemplate(name: string): string {
|
|
62
|
+
const hookName = `use${capitalize(name)}`;
|
|
63
|
+
return `import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
64
|
+
import { useTib } from '@/hooks/useTib';
|
|
65
|
+
import { toast } from 'sonner';
|
|
66
|
+
|
|
67
|
+
/** Input shape for the mutation. */
|
|
68
|
+
export interface ${capitalize(name)}Input {
|
|
69
|
+
workspaceId: string;
|
|
70
|
+
/** Fields to create or update. */
|
|
71
|
+
values: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Response from the API after a successful mutation. */
|
|
75
|
+
export interface ${capitalize(name)}Response {
|
|
76
|
+
id: string;
|
|
77
|
+
ok: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mutation hook for creating or updating ${name} entities.
|
|
82
|
+
*
|
|
83
|
+
* Uses TanStack Query \`useMutation\` with optimistic update support,
|
|
84
|
+
* automatic error rollback, and toast notifications.
|
|
85
|
+
*/
|
|
86
|
+
export function ${hookName}() {
|
|
87
|
+
const tib = useTib();
|
|
88
|
+
const queryClient = useQueryClient();
|
|
89
|
+
|
|
90
|
+
return useMutation<${capitalize(name)}Response, Error, ${capitalize(name)}Input>({
|
|
91
|
+
mutationFn: (input: ${capitalize(name)}Input) =>
|
|
92
|
+
tib.data.upsert({
|
|
93
|
+
workspaceId: input.workspaceId,
|
|
94
|
+
...input.values,
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
onMutate: async (input) => {
|
|
98
|
+
// Cancel outgoing refetches so they don't overwrite the optimistic update
|
|
99
|
+
const key = ['${name}-list', input.workspaceId];
|
|
100
|
+
await queryClient.cancelQueries({ queryKey: key });
|
|
101
|
+
|
|
102
|
+
// Snapshot the previous value for rollback
|
|
103
|
+
const previous = queryClient.getQueryData(key);
|
|
104
|
+
return { previous, key };
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
onError: (err, input, context) => {
|
|
108
|
+
// Roll back to the previous value on error
|
|
109
|
+
if (context?.previous) {
|
|
110
|
+
queryClient.setQueryData(context.key, context.previous);
|
|
111
|
+
}
|
|
112
|
+
toast.error('Operation failed', {
|
|
113
|
+
description: err.message,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
onSuccess: (_data, input) => {
|
|
118
|
+
toast.success('Operation successful');
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
onSettled: (_data, _error, input) => {
|
|
122
|
+
// Always refetch to ensure cache is fresh
|
|
123
|
+
queryClient.invalidateQueries({
|
|
124
|
+
queryKey: ['${name}', input.workspaceId],
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Mutation hook for deleting ${name} entities.
|
|
132
|
+
*/
|
|
133
|
+
export function useDelete${capitalize(name)}() {
|
|
134
|
+
const tib = useTib();
|
|
135
|
+
const queryClient = useQueryClient();
|
|
136
|
+
|
|
137
|
+
return useMutation<void, Error, { workspaceId: string; id: string }>({
|
|
138
|
+
mutationFn: ({ workspaceId, id }) =>
|
|
139
|
+
tib.data.delete({ workspaceId, id }),
|
|
140
|
+
|
|
141
|
+
onSuccess: (_data, { workspaceId }) => {
|
|
142
|
+
toast.success('Deleted');
|
|
143
|
+
queryClient.invalidateQueries({
|
|
144
|
+
queryKey: ['${name}', workspaceId],
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
onError: (err) => {
|
|
149
|
+
toast.error('Delete failed', {
|
|
150
|
+
description: err.message,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function featureFlagHookTemplate(name: string): string {
|
|
159
|
+
const hookName = `use${capitalize(name)}`;
|
|
160
|
+
return `import { useQuery } from '@tanstack/react-query';
|
|
161
|
+
import { useTib } from '@/hooks/useTib';
|
|
162
|
+
|
|
163
|
+
/** W6-style error shape returned by the TIB SDK. */
|
|
164
|
+
interface TibErrorShape {
|
|
165
|
+
code?: string;
|
|
166
|
+
traceId?: string;
|
|
167
|
+
message: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface FeatureFlagResult {
|
|
171
|
+
enabled: boolean;
|
|
172
|
+
config?: Record<string, unknown>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Hook that checks a feature flag via the TIB SDK.
|
|
177
|
+
*
|
|
178
|
+
* Uses TanStack Query with a 60-second stale time (matching the
|
|
179
|
+
* backend flag cache TTL). Returns \`{ enabled: false }\` on error
|
|
180
|
+
* so the UI degrades safely when the flags service is unreachable.
|
|
181
|
+
*/
|
|
182
|
+
export function ${hookName}(flagKey: string): {
|
|
183
|
+
enabled: boolean;
|
|
184
|
+
config?: Record<string, unknown>;
|
|
185
|
+
isLoading: boolean;
|
|
186
|
+
isError: boolean;
|
|
187
|
+
error: Error | null;
|
|
188
|
+
} {
|
|
189
|
+
const tib = useTib();
|
|
190
|
+
|
|
191
|
+
const { data, isLoading, isError, error } = useQuery<FeatureFlagResult, Error>({
|
|
192
|
+
queryKey: ['feature-flag', flagKey],
|
|
193
|
+
queryFn: async () => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await tib.flags.get(flagKey) as { value: boolean; config?: Record<string, unknown> };
|
|
196
|
+
return { enabled: result.value, config: result.config };
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// Degrade gracefully: return disabled on error
|
|
199
|
+
return { enabled: false };
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
staleTime: 60_000, // matches backend flag cache TTL
|
|
203
|
+
retry: 1,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
enabled: data?.enabled ?? false,
|
|
208
|
+
config: data?.config,
|
|
209
|
+
isLoading,
|
|
210
|
+
isError,
|
|
211
|
+
error,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function subscriptionHookTemplate(name: string): string {
|
|
218
|
+
const hookName = `use${capitalize(name)}`;
|
|
219
|
+
return `import { useEffect, useRef, useCallback } from 'react';
|
|
220
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
221
|
+
import { useTib } from '@/hooks/useTib';
|
|
222
|
+
|
|
223
|
+
/** Message envelope received over the subscription channel. */
|
|
224
|
+
interface ${capitalize(name)}Message {
|
|
225
|
+
eventId: string;
|
|
226
|
+
type: string;
|
|
227
|
+
payload: Record<string, unknown>;
|
|
228
|
+
timestamp: string;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Callback invoked for each received message. */
|
|
232
|
+
type ${capitalize(name)}Handler = (message: ${capitalize(name)}Message) => void;
|
|
233
|
+
|
|
234
|
+
/** Connection states for the subscription. */
|
|
235
|
+
type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Hook for subscribing to a real-time ${name} event stream.
|
|
239
|
+
*
|
|
240
|
+
* Uses EventSource (SSE) with automatic reconnection (exponential
|
|
241
|
+
* backoff, capped at 16 s). Incoming events are dispatched to the
|
|
242
|
+
* provided handler and optionally update TanStack Query caches.
|
|
243
|
+
*
|
|
244
|
+
* Heartbeat: server emits \`ping\` every 25 s. Missing two consecutive
|
|
245
|
+
* pings triggers reconnection.
|
|
246
|
+
*/
|
|
247
|
+
export function ${hookName}(
|
|
248
|
+
workspaceId: string,
|
|
249
|
+
onMessage?: ${capitalize(name)}Handler,
|
|
250
|
+
): {
|
|
251
|
+
status: ConnectionStatus;
|
|
252
|
+
disconnect: () => void;
|
|
253
|
+
reconnect: () => void;
|
|
254
|
+
} {
|
|
255
|
+
const tib = useTib();
|
|
256
|
+
const queryClient = useQueryClient();
|
|
257
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
258
|
+
const reconnectAttemptRef = useRef<number>(0);
|
|
259
|
+
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
260
|
+
const lastPingRef = useRef<number>(Date.now());
|
|
261
|
+
const pingCheckTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
262
|
+
const statusRef = useRef<ConnectionStatus>('disconnected');
|
|
263
|
+
|
|
264
|
+
const connect = useCallback(() => {
|
|
265
|
+
// Clean up any existing connection
|
|
266
|
+
if (eventSourceRef.current) {
|
|
267
|
+
eventSourceRef.current.close();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
statusRef.current = reconnectAttemptRef.current > 0 ? 'reconnecting' : 'connecting';
|
|
271
|
+
|
|
272
|
+
const url = new URL('\\\${import.meta.env.VITE_API_URL}/v1/stream');
|
|
273
|
+
url.searchParams.set('workspaceId', workspaceId);
|
|
274
|
+
|
|
275
|
+
const es = new EventSource(url.toString());
|
|
276
|
+
eventSourceRef.current = es;
|
|
277
|
+
lastPingRef.current = Date.now();
|
|
278
|
+
|
|
279
|
+
es.addEventListener('ping', () => {
|
|
280
|
+
lastPingRef.current = Date.now();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
es.addEventListener('message', (event) => {
|
|
284
|
+
try {
|
|
285
|
+
const data = JSON.parse(event.data) as ${capitalize(name)}Message;
|
|
286
|
+
statusRef.current = 'connected';
|
|
287
|
+
reconnectAttemptRef.current = 0;
|
|
288
|
+
|
|
289
|
+
if (onMessage) {
|
|
290
|
+
onMessage(data);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Optionally update a query cache key with the latest event
|
|
294
|
+
queryClient.setQueryData(['${name}-stream', workspaceId], data);
|
|
295
|
+
} catch {
|
|
296
|
+
// Ignore malformed messages
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
es.onerror = () => {
|
|
301
|
+
es.close();
|
|
302
|
+
scheduleReconnect();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
es.onopen = () => {
|
|
306
|
+
statusRef.current = 'connected';
|
|
307
|
+
reconnectAttemptRef.current = 0;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Start ping heartbeat checker
|
|
311
|
+
if (pingCheckTimerRef.current) {
|
|
312
|
+
clearInterval(pingCheckTimerRef.current);
|
|
313
|
+
}
|
|
314
|
+
pingCheckTimerRef.current = setInterval(() => {
|
|
315
|
+
const elapsed = Date.now() - lastPingRef.current;
|
|
316
|
+
if (elapsed > 50_000) {
|
|
317
|
+
// Missed two consecutive pings (> 50 s)
|
|
318
|
+
es.close();
|
|
319
|
+
scheduleReconnect();
|
|
320
|
+
}
|
|
321
|
+
}, 25_000);
|
|
322
|
+
}, [workspaceId, onMessage, queryClient]);
|
|
323
|
+
|
|
324
|
+
const scheduleReconnect = useCallback(() => {
|
|
325
|
+
if (reconnectTimerRef.current) {
|
|
326
|
+
clearTimeout(reconnectTimerRef.current);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const attempt = reconnectAttemptRef.current;
|
|
330
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 16_000) +
|
|
331
|
+
Math.random() * 1000; // ± jitter
|
|
332
|
+
|
|
333
|
+
reconnectAttemptRef.current = attempt + 1;
|
|
334
|
+
statusRef.current = 'reconnecting';
|
|
335
|
+
|
|
336
|
+
reconnectTimerRef.current = setTimeout(() => {
|
|
337
|
+
connect();
|
|
338
|
+
}, delay);
|
|
339
|
+
}, [connect]);
|
|
340
|
+
|
|
341
|
+
const disconnect = useCallback(() => {
|
|
342
|
+
if (eventSourceRef.current) {
|
|
343
|
+
eventSourceRef.current.close();
|
|
344
|
+
eventSourceRef.current = null;
|
|
345
|
+
}
|
|
346
|
+
if (reconnectTimerRef.current) {
|
|
347
|
+
clearTimeout(reconnectTimerRef.current);
|
|
348
|
+
reconnectTimerRef.current = null;
|
|
349
|
+
}
|
|
350
|
+
if (pingCheckTimerRef.current) {
|
|
351
|
+
clearInterval(pingCheckTimerRef.current);
|
|
352
|
+
pingCheckTimerRef.current = null;
|
|
353
|
+
}
|
|
354
|
+
statusRef.current = 'disconnected';
|
|
355
|
+
}, []);
|
|
356
|
+
|
|
357
|
+
const reconnect = useCallback(() => {
|
|
358
|
+
reconnectAttemptRef.current = 0;
|
|
359
|
+
disconnect();
|
|
360
|
+
connect();
|
|
361
|
+
}, [connect, disconnect]);
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
connect();
|
|
365
|
+
return () => disconnect();
|
|
366
|
+
}, [connect, disconnect]);
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
get status() {
|
|
370
|
+
return statusRef.current;
|
|
371
|
+
},
|
|
372
|
+
disconnect,
|
|
373
|
+
reconnect,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Capitalizes the first character of a string. */
|
|
380
|
+
function capitalize(s: string): string {
|
|
381
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
382
|
+
}
|