@lakutata/entrypoint 3.0.0-beta.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/LICENSE +23 -0
- package/README.md +183 -0
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/cjs/Entrypoint.d.ts +868 -0
- package/dist/cjs/Entrypoint.js +998 -0
- package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.d.ts +192 -0
- package/dist/cjs/adapter/commander/CommanderCLIEntrypoint.js +439 -0
- package/dist/cjs/adapter/commander/Prompt.d.ts +22 -0
- package/dist/cjs/adapter/commander/Prompt.js +164 -0
- package/dist/cjs/adapter/commander/Width.d.ts +12 -0
- package/dist/cjs/adapter/commander/Width.js +67 -0
- package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +203 -0
- package/dist/cjs/adapter/electron/ElectronIPCServiceEntrypoint.js +249 -0
- package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.d.ts +172 -0
- package/dist/cjs/adapter/express/ExpressHTTPEntrypoint.js +301 -0
- package/dist/cjs/adapter/express/Responders.d.ts +89 -0
- package/dist/cjs/adapter/express/Responders.js +71 -0
- package/dist/cjs/adapter/express/StaticUI.d.ts +83 -0
- package/dist/cjs/adapter/express/StaticUI.js +104 -0
- package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.d.ts +156 -0
- package/dist/cjs/adapter/fastify/FastifyHTTPEntrypoint.js +339 -0
- package/dist/cjs/adapter/fastify/Responders.d.ts +88 -0
- package/dist/cjs/adapter/fastify/Responders.js +77 -0
- package/dist/cjs/adapter/http/HTTPServiceEntrypoint.d.ts +117 -0
- package/dist/cjs/adapter/http/HTTPServiceEntrypoint.js +257 -0
- package/dist/cjs/adapter/jsonrpc/JSONRPCProtocol.d.ts +194 -0
- package/dist/cjs/adapter/jsonrpc/JSONRPCProtocol.js +187 -0
- package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +154 -0
- package/dist/cjs/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +397 -0
- package/dist/cjs/adapter/schedule/ScheduleEntrypoint.d.ts +86 -0
- package/dist/cjs/adapter/schedule/ScheduleEntrypoint.js +275 -0
- package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +138 -0
- package/dist/cjs/adapter/socket.io/SocketIOServiceEntrypoint.js +216 -0
- package/dist/cjs/channel/BuiltinChannels.d.ts +18 -0
- package/dist/cjs/channel/BuiltinChannels.js +128 -0
- package/dist/cjs/channel/Channel.d.ts +196 -0
- package/dist/cjs/channel/Channel.js +106 -0
- package/dist/cjs/channel/EntrypointAdapter.d.ts +195 -0
- package/dist/cjs/channel/EntrypointAdapter.js +43 -0
- package/dist/cjs/context/BaseContext.d.ts +75 -0
- package/dist/cjs/context/BaseContext.js +90 -0
- package/dist/cjs/context/CLIContext.d.ts +38 -0
- package/dist/cjs/context/CLIContext.js +53 -0
- package/dist/cjs/context/HTTPContext.d.ts +52 -0
- package/dist/cjs/context/HTTPContext.js +78 -0
- package/dist/cjs/context/ScheduleContext.d.ts +47 -0
- package/dist/cjs/context/ScheduleContext.js +65 -0
- package/dist/cjs/context/ServiceContext.d.ts +47 -0
- package/dist/cjs/context/ServiceContext.js +62 -0
- package/dist/cjs/decorators/CLIAction.d.ts +120 -0
- package/dist/cjs/decorators/CLIAction.js +13 -0
- package/dist/cjs/decorators/HTTPAction.d.ts +178 -0
- package/dist/cjs/decorators/HTTPAction.js +14 -0
- package/dist/cjs/decorators/Prefix.d.ts +44 -0
- package/dist/cjs/decorators/Prefix.js +48 -0
- package/dist/cjs/decorators/Schedule.d.ts +120 -0
- package/dist/cjs/decorators/Schedule.js +64 -0
- package/dist/cjs/decorators/ServiceAction.d.ts +116 -0
- package/dist/cjs/decorators/ServiceAction.js +12 -0
- package/dist/cjs/decorators/UseMiddlewares.d.ts +52 -0
- package/dist/cjs/decorators/UseMiddlewares.js +58 -0
- package/dist/cjs/decorators/http/DELETE.d.ts +87 -0
- package/dist/cjs/decorators/http/DELETE.js +7 -0
- package/dist/cjs/decorators/http/GET.d.ts +89 -0
- package/dist/cjs/decorators/http/GET.js +7 -0
- package/dist/cjs/decorators/http/HEAD.d.ts +84 -0
- package/dist/cjs/decorators/http/HEAD.js +7 -0
- package/dist/cjs/decorators/http/OPTIONS.d.ts +80 -0
- package/dist/cjs/decorators/http/OPTIONS.js +7 -0
- package/dist/cjs/decorators/http/PATCH.d.ts +90 -0
- package/dist/cjs/decorators/http/PATCH.js +7 -0
- package/dist/cjs/decorators/http/POST.d.ts +91 -0
- package/dist/cjs/decorators/http/POST.js +7 -0
- package/dist/cjs/decorators/http/PUT.d.ts +92 -0
- package/dist/cjs/decorators/http/PUT.js +7 -0
- package/dist/cjs/exceptions/AccessDeniedException.d.ts +10 -0
- package/dist/cjs/exceptions/AccessDeniedException.js +17 -0
- package/dist/cjs/exceptions/ControllerActionNotFoundException.d.ts +9 -0
- package/dist/cjs/exceptions/ControllerActionNotFoundException.js +16 -0
- package/dist/cjs/exceptions/DuplicateActionNameException.d.ts +8 -0
- package/dist/cjs/exceptions/DuplicateActionNameException.js +15 -0
- package/dist/cjs/exceptions/EntrypointStoppedException.d.ts +9 -0
- package/dist/cjs/exceptions/EntrypointStoppedException.js +16 -0
- package/dist/cjs/exceptions/InvalidActionGroupException.d.ts +4 -0
- package/dist/cjs/exceptions/InvalidActionGroupException.js +11 -0
- package/dist/cjs/exceptions/InvalidChannelException.d.ts +8 -0
- package/dist/cjs/exceptions/InvalidChannelException.js +15 -0
- package/dist/cjs/exceptions/InvalidEntrypointAdapterException.d.ts +9 -0
- package/dist/cjs/exceptions/InvalidEntrypointAdapterException.js +16 -0
- package/dist/cjs/exceptions/InvalidMiddlewareException.d.ts +8 -0
- package/dist/cjs/exceptions/InvalidMiddlewareException.js +15 -0
- package/dist/cjs/exceptions/InvalidPrefixException.d.ts +8 -0
- package/dist/cjs/exceptions/InvalidPrefixException.js +15 -0
- package/dist/cjs/exceptions/MiddlewareNextCalledException.d.ts +8 -0
- package/dist/cjs/exceptions/MiddlewareNextCalledException.js +15 -0
- package/dist/cjs/exceptions/RouteNotFoundException.d.ts +9 -0
- package/dist/cjs/exceptions/RouteNotFoundException.js +16 -0
- package/dist/cjs/exceptions/ServiceInvokeException.d.ts +11 -0
- package/dist/cjs/exceptions/ServiceInvokeException.js +14 -0
- package/dist/cjs/exports/CTRLDecorators.d.ts +13 -0
- package/dist/cjs/exports/CTRLDecorators.js +38 -0
- package/dist/cjs/exports/Channel.d.ts +14 -0
- package/dist/cjs/exports/Channel.js +33 -0
- package/dist/cjs/exports/CommanderAdapter.d.ts +1 -0
- package/dist/cjs/exports/CommanderAdapter.js +8 -0
- package/dist/cjs/exports/ElectronAdapter.d.ts +1 -0
- package/dist/cjs/exports/ElectronAdapter.js +9 -0
- package/dist/cjs/exports/Entrypoint.d.ts +24 -0
- package/dist/cjs/exports/Entrypoint.js +55 -0
- package/dist/cjs/exports/ExpressAdapter.d.ts +4 -0
- package/dist/cjs/exports/ExpressAdapter.js +15 -0
- package/dist/cjs/exports/FastifyAdapter.d.ts +4 -0
- package/dist/cjs/exports/FastifyAdapter.js +14 -0
- package/dist/cjs/exports/JSONRPCAdapter.d.ts +2 -0
- package/dist/cjs/exports/JSONRPCAdapter.js +11 -0
- package/dist/cjs/exports/SocketIOAdapter.d.ts +1 -0
- package/dist/cjs/exports/SocketIOAdapter.js +9 -0
- package/dist/cjs/lib/AccessControl.d.ts +118 -0
- package/dist/cjs/lib/AccessControl.js +208 -0
- package/dist/cjs/lib/AccessControlRule.d.ts +91 -0
- package/dist/cjs/lib/AccessControlRule.js +76 -0
- package/dist/cjs/lib/ActionOptions.d.ts +67 -0
- package/dist/cjs/lib/ActionOptions.js +67 -0
- package/dist/cjs/lib/ActionResult.d.ts +28 -0
- package/dist/cjs/lib/ActionResult.js +28 -0
- package/dist/cjs/lib/ApplicationOf.d.ts +6 -0
- package/dist/cjs/lib/ApplicationOf.js +14 -0
- package/dist/cjs/lib/Controller.d.ts +59 -0
- package/dist/cjs/lib/Controller.js +64 -0
- package/dist/cjs/lib/ControllerEntrypoint.d.ts +168 -0
- package/dist/cjs/lib/ControllerEntrypoint.js +239 -0
- package/dist/cjs/lib/GetActionDTOAndOptions.d.ts +29 -0
- package/dist/cjs/lib/GetActionDTOAndOptions.js +49 -0
- package/dist/cjs/lib/HTTPErrors.d.ts +55 -0
- package/dist/cjs/lib/HTTPErrors.js +52 -0
- package/dist/cjs/lib/Middleware.d.ts +146 -0
- package/dist/cjs/lib/Middleware.js +144 -0
- package/dist/cjs/lib/Prefix.d.ts +98 -0
- package/dist/cjs/lib/Prefix.js +151 -0
- package/dist/cjs/lib/Result.d.ts +173 -0
- package/dist/cjs/lib/Result.js +153 -0
- package/dist/cjs/lib/ServiceResponseCodec.d.ts +82 -0
- package/dist/cjs/lib/ServiceResponseCodec.js +117 -0
- package/dist/cjs/lib/StringifyPattern.d.ts +7 -0
- package/dist/cjs/lib/StringifyPattern.js +58 -0
- package/dist/cjs/lib/UnsetValue.d.ts +12 -0
- package/dist/cjs/lib/UnsetValue.js +216 -0
- package/dist/cjs/lib/pattern/GlobPattern.d.ts +34 -0
- package/dist/cjs/lib/pattern/GlobPattern.js +68 -0
- package/dist/cjs/lib/pattern/PatternManager.d.ts +103 -0
- package/dist/cjs/lib/pattern/PatternManager.js +233 -0
- package/dist/cjs/lib/schedule/Cron.d.ts +56 -0
- package/dist/cjs/lib/schedule/Cron.js +282 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/CLIContextParams.d.ts +14 -0
- package/dist/cjs/types/CLIContextParams.js +2 -0
- package/dist/cjs/types/ErrorDescription.d.ts +14 -0
- package/dist/cjs/types/ErrorDescription.js +2 -0
- package/dist/cjs/types/HTTPContextParams.d.ts +27 -0
- package/dist/cjs/types/HTTPContextParams.js +2 -0
- package/dist/cjs/types/ScheduleContextParams.d.ts +21 -0
- package/dist/cjs/types/ScheduleContextParams.js +2 -0
- package/dist/cjs/types/ServiceContextParams.d.ts +16 -0
- package/dist/cjs/types/ServiceContextParams.js +2 -0
- package/dist/esm/Entrypoint.js +985 -0
- package/dist/esm/adapter/commander/CommanderCLIEntrypoint.js +433 -0
- package/dist/esm/adapter/commander/Prompt.js +127 -0
- package/dist/esm/adapter/commander/Width.js +63 -0
- package/dist/esm/adapter/electron/ElectronIPCServiceEntrypoint.js +212 -0
- package/dist/esm/adapter/express/ExpressHTTPEntrypoint.js +295 -0
- package/dist/esm/adapter/express/Responders.js +66 -0
- package/dist/esm/adapter/express/StaticUI.js +96 -0
- package/dist/esm/adapter/fastify/FastifyHTTPEntrypoint.js +333 -0
- package/dist/esm/adapter/fastify/Responders.js +73 -0
- package/dist/esm/adapter/http/HTTPServiceEntrypoint.js +253 -0
- package/dist/esm/adapter/jsonrpc/JSONRPCProtocol.js +182 -0
- package/dist/esm/adapter/jsonrpc/JSONRPCServiceEntrypoint.js +361 -0
- package/dist/esm/adapter/schedule/ScheduleEntrypoint.js +272 -0
- package/dist/esm/adapter/socket.io/SocketIOServiceEntrypoint.js +212 -0
- package/dist/esm/channel/BuiltinChannels.js +125 -0
- package/dist/esm/channel/Channel.js +100 -0
- package/dist/esm/channel/EntrypointAdapter.js +39 -0
- package/dist/esm/context/BaseContext.js +87 -0
- package/dist/esm/context/CLIContext.js +49 -0
- package/dist/esm/context/HTTPContext.js +74 -0
- package/dist/esm/context/ScheduleContext.js +61 -0
- package/dist/esm/context/ServiceContext.js +58 -0
- package/dist/esm/decorators/CLIAction.js +10 -0
- package/dist/esm/decorators/HTTPAction.js +11 -0
- package/dist/esm/decorators/Prefix.js +45 -0
- package/dist/esm/decorators/Schedule.js +61 -0
- package/dist/esm/decorators/ServiceAction.js +9 -0
- package/dist/esm/decorators/UseMiddlewares.js +55 -0
- package/dist/esm/decorators/http/DELETE.js +4 -0
- package/dist/esm/decorators/http/GET.js +4 -0
- package/dist/esm/decorators/http/HEAD.js +4 -0
- package/dist/esm/decorators/http/OPTIONS.js +4 -0
- package/dist/esm/decorators/http/PATCH.js +4 -0
- package/dist/esm/decorators/http/POST.js +4 -0
- package/dist/esm/decorators/http/PUT.js +4 -0
- package/dist/esm/exceptions/AccessDeniedException.js +13 -0
- package/dist/esm/exceptions/ControllerActionNotFoundException.js +12 -0
- package/dist/esm/exceptions/DuplicateActionNameException.js +11 -0
- package/dist/esm/exceptions/EntrypointStoppedException.js +12 -0
- package/dist/esm/exceptions/InvalidActionGroupException.js +7 -0
- package/dist/esm/exceptions/InvalidChannelException.js +11 -0
- package/dist/esm/exceptions/InvalidEntrypointAdapterException.js +12 -0
- package/dist/esm/exceptions/InvalidMiddlewareException.js +11 -0
- package/dist/esm/exceptions/InvalidPrefixException.js +11 -0
- package/dist/esm/exceptions/MiddlewareNextCalledException.js +11 -0
- package/dist/esm/exceptions/RouteNotFoundException.js +12 -0
- package/dist/esm/exceptions/ServiceInvokeException.js +10 -0
- package/dist/esm/exports/CTRLDecorators.js +19 -0
- package/dist/esm/exports/Channel.js +11 -0
- package/dist/esm/exports/CommanderAdapter.js +1 -0
- package/dist/esm/exports/ElectronAdapter.js +1 -0
- package/dist/esm/exports/Entrypoint.js +22 -0
- package/dist/esm/exports/ExpressAdapter.js +3 -0
- package/dist/esm/exports/FastifyAdapter.js +3 -0
- package/dist/esm/exports/JSONRPCAdapter.js +2 -0
- package/dist/esm/exports/SocketIOAdapter.js +1 -0
- package/dist/esm/lib/AccessControl.js +204 -0
- package/dist/esm/lib/AccessControlRule.js +72 -0
- package/dist/esm/lib/ActionOptions.js +63 -0
- package/dist/esm/lib/ActionResult.js +24 -0
- package/dist/esm/lib/ApplicationOf.js +11 -0
- package/dist/esm/lib/Controller.js +61 -0
- package/dist/esm/lib/ControllerEntrypoint.js +229 -0
- package/dist/esm/lib/GetActionDTOAndOptions.js +46 -0
- package/dist/esm/lib/HTTPErrors.js +48 -0
- package/dist/esm/lib/Middleware.js +134 -0
- package/dist/esm/lib/Prefix.js +143 -0
- package/dist/esm/lib/Result.js +147 -0
- package/dist/esm/lib/ServiceResponseCodec.js +113 -0
- package/dist/esm/lib/StringifyPattern.js +55 -0
- package/dist/esm/lib/UnsetValue.js +213 -0
- package/dist/esm/lib/pattern/GlobPattern.js +64 -0
- package/dist/esm/lib/pattern/PatternManager.js +229 -0
- package/dist/esm/lib/schedule/Cron.js +277 -0
- package/dist/esm/types/CLIContextParams.js +1 -0
- package/dist/esm/types/ErrorDescription.js +1 -0
- package/dist/esm/types/HTTPContextParams.js +1 -0
- package/dist/esm/types/ScheduleContextParams.js +1 -0
- package/dist/esm/types/ServiceContextParams.js +1 -0
- package/dist/types/Entrypoint.d.ts +868 -0
- package/dist/types/adapter/commander/CommanderCLIEntrypoint.d.ts +192 -0
- package/dist/types/adapter/commander/Prompt.d.ts +22 -0
- package/dist/types/adapter/commander/Width.d.ts +12 -0
- package/dist/types/adapter/electron/ElectronIPCServiceEntrypoint.d.ts +203 -0
- package/dist/types/adapter/express/ExpressHTTPEntrypoint.d.ts +172 -0
- package/dist/types/adapter/express/Responders.d.ts +89 -0
- package/dist/types/adapter/express/StaticUI.d.ts +83 -0
- package/dist/types/adapter/fastify/FastifyHTTPEntrypoint.d.ts +156 -0
- package/dist/types/adapter/fastify/Responders.d.ts +88 -0
- package/dist/types/adapter/http/HTTPServiceEntrypoint.d.ts +117 -0
- package/dist/types/adapter/jsonrpc/JSONRPCProtocol.d.ts +194 -0
- package/dist/types/adapter/jsonrpc/JSONRPCServiceEntrypoint.d.ts +154 -0
- package/dist/types/adapter/schedule/ScheduleEntrypoint.d.ts +86 -0
- package/dist/types/adapter/socket.io/SocketIOServiceEntrypoint.d.ts +138 -0
- package/dist/types/channel/BuiltinChannels.d.ts +18 -0
- package/dist/types/channel/Channel.d.ts +196 -0
- package/dist/types/channel/EntrypointAdapter.d.ts +195 -0
- package/dist/types/context/BaseContext.d.ts +75 -0
- package/dist/types/context/CLIContext.d.ts +38 -0
- package/dist/types/context/HTTPContext.d.ts +52 -0
- package/dist/types/context/ScheduleContext.d.ts +47 -0
- package/dist/types/context/ServiceContext.d.ts +47 -0
- package/dist/types/decorators/CLIAction.d.ts +120 -0
- package/dist/types/decorators/HTTPAction.d.ts +178 -0
- package/dist/types/decorators/Prefix.d.ts +44 -0
- package/dist/types/decorators/Schedule.d.ts +120 -0
- package/dist/types/decorators/ServiceAction.d.ts +116 -0
- package/dist/types/decorators/UseMiddlewares.d.ts +52 -0
- package/dist/types/decorators/http/DELETE.d.ts +87 -0
- package/dist/types/decorators/http/GET.d.ts +89 -0
- package/dist/types/decorators/http/HEAD.d.ts +84 -0
- package/dist/types/decorators/http/OPTIONS.d.ts +80 -0
- package/dist/types/decorators/http/PATCH.d.ts +90 -0
- package/dist/types/decorators/http/POST.d.ts +91 -0
- package/dist/types/decorators/http/PUT.d.ts +92 -0
- package/dist/types/exceptions/AccessDeniedException.d.ts +10 -0
- package/dist/types/exceptions/ControllerActionNotFoundException.d.ts +9 -0
- package/dist/types/exceptions/DuplicateActionNameException.d.ts +8 -0
- package/dist/types/exceptions/EntrypointStoppedException.d.ts +9 -0
- package/dist/types/exceptions/InvalidActionGroupException.d.ts +4 -0
- package/dist/types/exceptions/InvalidChannelException.d.ts +8 -0
- package/dist/types/exceptions/InvalidEntrypointAdapterException.d.ts +9 -0
- package/dist/types/exceptions/InvalidMiddlewareException.d.ts +8 -0
- package/dist/types/exceptions/InvalidPrefixException.d.ts +8 -0
- package/dist/types/exceptions/MiddlewareNextCalledException.d.ts +8 -0
- package/dist/types/exceptions/RouteNotFoundException.d.ts +9 -0
- package/dist/types/exceptions/ServiceInvokeException.d.ts +11 -0
- package/dist/types/exports/CTRLDecorators.d.ts +13 -0
- package/dist/types/exports/Channel.d.ts +14 -0
- package/dist/types/exports/CommanderAdapter.d.ts +1 -0
- package/dist/types/exports/ElectronAdapter.d.ts +1 -0
- package/dist/types/exports/Entrypoint.d.ts +24 -0
- package/dist/types/exports/ExpressAdapter.d.ts +4 -0
- package/dist/types/exports/FastifyAdapter.d.ts +4 -0
- package/dist/types/exports/JSONRPCAdapter.d.ts +2 -0
- package/dist/types/exports/SocketIOAdapter.d.ts +1 -0
- package/dist/types/lib/AccessControl.d.ts +118 -0
- package/dist/types/lib/AccessControlRule.d.ts +91 -0
- package/dist/types/lib/ActionOptions.d.ts +67 -0
- package/dist/types/lib/ActionResult.d.ts +28 -0
- package/dist/types/lib/ApplicationOf.d.ts +6 -0
- package/dist/types/lib/Controller.d.ts +59 -0
- package/dist/types/lib/ControllerEntrypoint.d.ts +168 -0
- package/dist/types/lib/GetActionDTOAndOptions.d.ts +29 -0
- package/dist/types/lib/HTTPErrors.d.ts +55 -0
- package/dist/types/lib/Middleware.d.ts +146 -0
- package/dist/types/lib/Prefix.d.ts +98 -0
- package/dist/types/lib/Result.d.ts +173 -0
- package/dist/types/lib/ServiceResponseCodec.d.ts +82 -0
- package/dist/types/lib/StringifyPattern.d.ts +7 -0
- package/dist/types/lib/UnsetValue.d.ts +12 -0
- package/dist/types/lib/pattern/GlobPattern.d.ts +34 -0
- package/dist/types/lib/pattern/PatternManager.d.ts +103 -0
- package/dist/types/lib/schedule/Cron.d.ts +56 -0
- package/dist/types/types/CLIContextParams.d.ts +14 -0
- package/dist/types/types/ErrorDescription.d.ts +14 -0
- package/dist/types/types/HTTPContextParams.d.ts +27 -0
- package/dist/types/types/ScheduleContextParams.d.ts +21 -0
- package/dist/types/types/ServiceContextParams.d.ts +16 -0
- package/package.json +152 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { HTTPContext } from '../../context/HTTPContext.js';
|
|
5
|
+
import { HTTPChannel } from '../../channel/BuiltinChannels.js';
|
|
6
|
+
import { EventsResult, FileResult, StatusResult } from '../../lib/Result.js';
|
|
7
|
+
import { RouteNotFoundException } from '../../exceptions/RouteNotFoundException.js';
|
|
8
|
+
import { FastifyJSONResponder } from './Responders.js';
|
|
9
|
+
/**
|
|
10
|
+
* The content types of the common file extensions
|
|
11
|
+
*/
|
|
12
|
+
const CONTENT_TYPES = {
|
|
13
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
14
|
+
'.html': 'text/html; charset=utf-8',
|
|
15
|
+
'.css': 'text/css; charset=utf-8',
|
|
16
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
17
|
+
'.json': 'application/json; charset=utf-8',
|
|
18
|
+
'.csv': 'text/csv; charset=utf-8',
|
|
19
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
20
|
+
'.png': 'image/png',
|
|
21
|
+
'.jpg': 'image/jpeg',
|
|
22
|
+
'.jpeg': 'image/jpeg',
|
|
23
|
+
'.gif': 'image/gif',
|
|
24
|
+
'.svg': 'image/svg+xml',
|
|
25
|
+
'.webp': 'image/webp',
|
|
26
|
+
'.pdf': 'application/pdf',
|
|
27
|
+
'.zip': 'application/zip'
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The first segments of the routes
|
|
31
|
+
* @param routes
|
|
32
|
+
*/
|
|
33
|
+
function actionPrefixesOf(routes) {
|
|
34
|
+
const prefixes = new Set();
|
|
35
|
+
for (const route of routes) {
|
|
36
|
+
const segment = route.split('/').find((part) => !!part);
|
|
37
|
+
//A route starting with a parameter matches every path
|
|
38
|
+
if (segment && !/^[:*]/.test(segment))
|
|
39
|
+
prefixes.add(`/${segment}`);
|
|
40
|
+
}
|
|
41
|
+
return [...prefixes];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The data of an event of a stream
|
|
45
|
+
* @param data
|
|
46
|
+
* @param event
|
|
47
|
+
* @param id
|
|
48
|
+
*/
|
|
49
|
+
function eventFrame(data, event, id) {
|
|
50
|
+
let frame = '';
|
|
51
|
+
if (id !== undefined)
|
|
52
|
+
frame += `id: ${id}\n`;
|
|
53
|
+
if (event !== undefined)
|
|
54
|
+
frame += `event: ${event}\n`;
|
|
55
|
+
const text = typeof data === 'string' ? data : JSON.stringify(data);
|
|
56
|
+
for (const line of text.split(/\r?\n/))
|
|
57
|
+
frame += `data: ${line}\n`;
|
|
58
|
+
return `${frame}\n`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether a value is a fastify instance
|
|
62
|
+
* @param value
|
|
63
|
+
*/
|
|
64
|
+
function isFastifyInstance(value) {
|
|
65
|
+
return !!value && typeof value.route === 'function' && typeof value.listen === 'function';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create the HTTP adapter of an entrypoint served by a fastify instance: each HTTP action (`@GET`, `@POST`,
|
|
69
|
+
* `@HTTPAction`…) becomes a fastify route. It needs the optional peer dependency `fastify`.
|
|
70
|
+
*
|
|
71
|
+
* An action's input is the request's query and body (see the `input` option) with the route parameters merged over
|
|
72
|
+
* them. Its result is written by the responder (JSON by default, 204 for no result); a `StatusResult` sets the status
|
|
73
|
+
* and the headers, a `FileResult` sends a file, a buffer or a stream (its content type guessed from the extension of
|
|
74
|
+
* its file name, 304 when its ETag matches `If-None-Match`), and an `EventsResult` streams Server-Sent Events. An action
|
|
75
|
+
* that writes the response itself (through `HTTPContext.response`) is left alone. The errors, those of fastify included
|
|
76
|
+
* (a body too large: 413, a malformed JSON body: 400), are written by the responder with their status (500 by default).
|
|
77
|
+
* A GET action also answers HEAD, unless a HEAD action is declared for its route.
|
|
78
|
+
*
|
|
79
|
+
* When the client disconnects before the response, the action's abort signal fires and nothing is written. When the
|
|
80
|
+
* entrypoint stops, fastify stops accepting connections and answers the new requests with 503, the event streams end,
|
|
81
|
+
* and the requests in progress are given up to one second after they settle to send their responses before the
|
|
82
|
+
* remaining connections are closed.
|
|
83
|
+
* @param options The adapter options; by default it listens on port 80 with a new fastify instance.
|
|
84
|
+
* @returns The adapter, to give to the `http` option of `buildEntrypoints`.
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import {Application} from 'lakutata'
|
|
88
|
+
* import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
89
|
+
* import {GET, POST} from 'lakutata/decorator/ctrl'
|
|
90
|
+
* import {fastifyHTTPEntrypoint} from 'lakutata/adapter/fastify'
|
|
91
|
+
*
|
|
92
|
+
* class UserController extends Controller {
|
|
93
|
+
* //GET /users/1?verbose=true: the route parameter id merged over the query
|
|
94
|
+
* @GET('/users/:id')
|
|
95
|
+
* public async get(input: {id: string, verbose?: string}): Promise<{id: string}> {
|
|
96
|
+
* return {id: input.id}
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* //POST /users with the JSON body {"name": "Ada"}
|
|
100
|
+
* @POST('/users')
|
|
101
|
+
* public async create(input: {name: string}): Promise<{name: string}> {
|
|
102
|
+
* return {name: input.name}
|
|
103
|
+
* }
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* Application.run({
|
|
107
|
+
* id: 'web.app',
|
|
108
|
+
* name: 'Web',
|
|
109
|
+
* components: {
|
|
110
|
+
* entrypoint: buildEntrypoints({
|
|
111
|
+
* controllers: [UserController],
|
|
112
|
+
* http: fastifyHTTPEntrypoint({
|
|
113
|
+
* port: 8080,
|
|
114
|
+
* host: '0.0.0.0',
|
|
115
|
+
* fastify: {bodyLimit: 5 * 1024 * 1024, trustProxy: true},
|
|
116
|
+
* //The application's own routes
|
|
117
|
+
* before: (app): void => {
|
|
118
|
+
* app.get('/health', async (): Promise<string> => 'ok')
|
|
119
|
+
* }
|
|
120
|
+
* })
|
|
121
|
+
* })
|
|
122
|
+
* }
|
|
123
|
+
* })
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function fastifyHTTPEntrypoint(options = {}) {
|
|
127
|
+
const respond = options.respond ?? FastifyJSONResponder;
|
|
128
|
+
const input = options.input ?? ((request) => ({
|
|
129
|
+
...(request.query && typeof request.query === 'object' ? request.query : {}),
|
|
130
|
+
...(request.body && typeof request.body === 'object' && !Array.isArray(request.body) ? request.body : {})
|
|
131
|
+
}));
|
|
132
|
+
const streams = new Set();
|
|
133
|
+
let app;
|
|
134
|
+
let closed;
|
|
135
|
+
let stopping = false;
|
|
136
|
+
//The responses not sent yet
|
|
137
|
+
const responding = new Set();
|
|
138
|
+
/**
|
|
139
|
+
* Stream the events of a result
|
|
140
|
+
* @param result
|
|
141
|
+
* @param reply
|
|
142
|
+
*/
|
|
143
|
+
const streamEvents = async (result, reply) => {
|
|
144
|
+
const abortController = new AbortController();
|
|
145
|
+
streams.add(abortController);
|
|
146
|
+
reply.hijack();
|
|
147
|
+
const response = reply.raw;
|
|
148
|
+
response.writeHead(200, {
|
|
149
|
+
...reply.getHeaders(),
|
|
150
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
151
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
152
|
+
'Connection': 'keep-alive',
|
|
153
|
+
'X-Accel-Buffering': 'no'
|
|
154
|
+
});
|
|
155
|
+
response.flushHeaders();
|
|
156
|
+
const heartbeat = setInterval(() => response.write(': ping\n\n'), options.heartbeat ?? 15000);
|
|
157
|
+
const close = () => abortController.abort();
|
|
158
|
+
response.on('close', close);
|
|
159
|
+
try {
|
|
160
|
+
await result.producer((data, event, id) => {
|
|
161
|
+
if (!abortController.signal.aborted)
|
|
162
|
+
response.write(eventFrame(data, event, id));
|
|
163
|
+
}, abortController.signal);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
//The stream is already started, its error ends it
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
clearInterval(heartbeat);
|
|
170
|
+
response.off('close', close);
|
|
171
|
+
streams.delete(abortController);
|
|
172
|
+
if (!response.writableEnded)
|
|
173
|
+
response.end();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Send a file
|
|
178
|
+
* @param result
|
|
179
|
+
* @param request
|
|
180
|
+
* @param reply
|
|
181
|
+
*/
|
|
182
|
+
const sendFile = async (result, request, reply) => {
|
|
183
|
+
const { contentType, filename, etag, headers } = result.options;
|
|
184
|
+
if (headers)
|
|
185
|
+
reply.headers(headers);
|
|
186
|
+
if (etag) {
|
|
187
|
+
reply.header('ETag', `"${etag}"`);
|
|
188
|
+
if (request.headers['if-none-match'] === `"${etag}"`) {
|
|
189
|
+
await reply.code(304).send();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const source = result.source;
|
|
194
|
+
const name = filename ?? (typeof source === 'string' ? path.basename(source) : undefined);
|
|
195
|
+
if (filename)
|
|
196
|
+
reply.header('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
|
|
197
|
+
reply.type(contentType ?? (name && CONTENT_TYPES[path.extname(name).toLowerCase()]) ?? 'application/octet-stream');
|
|
198
|
+
await reply.send(typeof source === 'string' ? createReadStream(path.resolve(source)) : source);
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Write the result of an action
|
|
202
|
+
* @param result
|
|
203
|
+
* @param request
|
|
204
|
+
* @param reply
|
|
205
|
+
*/
|
|
206
|
+
const output = async (result, request, reply) => {
|
|
207
|
+
//The action wrote the response itself
|
|
208
|
+
if (reply.sent || reply.raw.headersSent)
|
|
209
|
+
return;
|
|
210
|
+
if (result instanceof EventsResult)
|
|
211
|
+
return await streamEvents(result, reply);
|
|
212
|
+
if (result instanceof FileResult)
|
|
213
|
+
return await sendFile(result, request, reply);
|
|
214
|
+
if (result instanceof StatusResult) {
|
|
215
|
+
reply.code(result.status).headers(result.headers);
|
|
216
|
+
return respond.success(reply, result.data, request);
|
|
217
|
+
}
|
|
218
|
+
respond.success(reply, result, request);
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* The handler of the requests of an action
|
|
222
|
+
* @param runtime
|
|
223
|
+
* @param action
|
|
224
|
+
*/
|
|
225
|
+
const actionHandler = (runtime, action) => async (request, reply) => {
|
|
226
|
+
const abortController = new AbortController();
|
|
227
|
+
//The client left before the response was sent
|
|
228
|
+
reply.raw.once('close', () => {
|
|
229
|
+
if (!reply.raw.writableFinished)
|
|
230
|
+
abortController.abort();
|
|
231
|
+
});
|
|
232
|
+
const context = new HTTPContext({
|
|
233
|
+
route: action.address.route,
|
|
234
|
+
method: action.address.method,
|
|
235
|
+
request: request.raw,
|
|
236
|
+
response: reply.raw,
|
|
237
|
+
data: input(request)
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
const result = await runtime.dispatchTo(action, context, { signal: abortController.signal, params: { ...request.params } });
|
|
241
|
+
if (!abortController.signal.aborted)
|
|
242
|
+
await output(result, request, reply);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (!reply.sent && !reply.raw.headersSent && !abortController.signal.aborted)
|
|
246
|
+
respond.error(reply, error, request, 'action');
|
|
247
|
+
}
|
|
248
|
+
//The client left: nothing is sent, the request is done for fastify (it is not waited for when fastify closes)
|
|
249
|
+
if (abortController.signal.aborted && !reply.sent) {
|
|
250
|
+
reply.hijack();
|
|
251
|
+
reply.raw.destroy();
|
|
252
|
+
}
|
|
253
|
+
return reply;
|
|
254
|
+
};
|
|
255
|
+
return {
|
|
256
|
+
channel: 'http',
|
|
257
|
+
definition: HTTPChannel,
|
|
258
|
+
name: options.name,
|
|
259
|
+
middlewares: options.middlewares,
|
|
260
|
+
start: async (runtime) => {
|
|
261
|
+
const fastify = isFastifyInstance(options.fastify) ? options.fastify : Fastify(options.fastify ?? {});
|
|
262
|
+
app = fastify;
|
|
263
|
+
const routes = new Map();
|
|
264
|
+
for (const action of runtime.actions)
|
|
265
|
+
routes.set(action.address.route, (routes.get(action.address.route) ?? new Set()).add(String(action.address.method).toUpperCase()));
|
|
266
|
+
const actionPrefixes = actionPrefixesOf(routes.keys());
|
|
267
|
+
const isActionPath = (requestPath) => actionPrefixes.some((prefix) => requestPath === prefix || requestPath.startsWith(`${prefix}/`));
|
|
268
|
+
await options.before?.(fastify, { module: runtime.module, server: fastify.server, routes: routes, actionPrefixes: actionPrefixes, isActionPath: isActionPath });
|
|
269
|
+
for (const action of runtime.actions) {
|
|
270
|
+
const method = String(action.address.method).toUpperCase();
|
|
271
|
+
fastify.route({
|
|
272
|
+
method: method,
|
|
273
|
+
url: action.address.route,
|
|
274
|
+
//A HEAD action of the route replaces the HEAD route fastify adds to a GET route
|
|
275
|
+
exposeHeadRoute: method === 'GET' && !routes.get(action.address.route).has('HEAD'),
|
|
276
|
+
handler: actionHandler(runtime, action)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
fastify.setNotFoundHandler((request, reply) => {
|
|
280
|
+
const requestPath = request.url.split('?')[0];
|
|
281
|
+
if (options.fallback && !isActionPath(requestPath))
|
|
282
|
+
return options.fallback(request, reply);
|
|
283
|
+
return respond.error(reply, new RouteNotFoundException('Route {route} not found', { route: requestPath }), request, 'request');
|
|
284
|
+
});
|
|
285
|
+
//The responses in progress are waited for when the adapter closes
|
|
286
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
287
|
+
const responded = new Promise((resolve) => void reply.raw.once('close', resolve));
|
|
288
|
+
responding.add(responded);
|
|
289
|
+
//Only resolved
|
|
290
|
+
void responded.then(() => responding.delete(responded));
|
|
291
|
+
});
|
|
292
|
+
//While the adapter stops, the connections are closed once their response is sent
|
|
293
|
+
fastify.addHook('onResponse', async () => {
|
|
294
|
+
if (stopping)
|
|
295
|
+
setImmediate(() => fastify.server.closeIdleConnections());
|
|
296
|
+
});
|
|
297
|
+
fastify.setErrorHandler((error, request, reply) => respond.error(reply, error, request, 'request'));
|
|
298
|
+
//The adapter starts once the server listens (an occupied port fails the start)
|
|
299
|
+
//Every interface by default, as the other adapters (fastify listens on localhost only)
|
|
300
|
+
if (options.host !== undefined) {
|
|
301
|
+
await fastify.listen({ port: options.port ?? 80, host: options.host });
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
try {
|
|
305
|
+
await fastify.listen({ port: options.port ?? 80, host: '::' });
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!['EAFNOSUPPORT', 'EADDRNOTAVAIL'].includes(error.code ?? ''))
|
|
309
|
+
throw error;
|
|
310
|
+
await fastify.listen({ port: options.port ?? 80, host: '0.0.0.0' });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
stop: () => {
|
|
315
|
+
//The event streams end; fastify stops accepting connections, closes the idle ones and answers 503 to the new requests
|
|
316
|
+
stopping = true;
|
|
317
|
+
streams.forEach((stream) => stream.abort());
|
|
318
|
+
closed = app?.close();
|
|
319
|
+
},
|
|
320
|
+
close: async () => {
|
|
321
|
+
//The requests in progress ended or were aborted: their responses are given a second to be sent, then the remaining connections are closed
|
|
322
|
+
//(a connection opened without a request would hold the close of the server)
|
|
323
|
+
let timer;
|
|
324
|
+
await Promise.race([Promise.all(responding), new Promise((resolve) => {
|
|
325
|
+
timer = setTimeout(resolve, 1000);
|
|
326
|
+
})]);
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
app?.server.closeAllConnections();
|
|
329
|
+
await closed;
|
|
330
|
+
await options.stop?.();
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describeError, statusOfError } from '../../lib/HTTPErrors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Send JSON
|
|
4
|
+
* @param reply
|
|
5
|
+
* @param data
|
|
6
|
+
*/
|
|
7
|
+
function sendJSON(reply, data) {
|
|
8
|
+
reply.type('application/json; charset=utf-8').send(JSON.stringify(data));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The default responder of {@link fastifyHTTPEntrypoint}: a result is sent as JSON, no result as an empty 204 (or the
|
|
12
|
+
* status set by a `StatusResult`); an error is sent as `{errno, errMsg}` (see `describeError`) with its HTTP status
|
|
13
|
+
* (see `statusOfError`: the first 4xx status in its cause chain, its own status, or 500).
|
|
14
|
+
*/
|
|
15
|
+
export const FastifyJSONResponder = {
|
|
16
|
+
success: (reply, data) => {
|
|
17
|
+
if (data === undefined) {
|
|
18
|
+
reply.code(reply.statusCode === 200 ? 204 : reply.statusCode).send();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
sendJSON(reply, data);
|
|
22
|
+
},
|
|
23
|
+
error: (reply, error) => {
|
|
24
|
+
reply.code(statusOfError(error));
|
|
25
|
+
sendJSON(reply, describeError(error));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Create a responder for {@link fastifyHTTPEntrypoint} that wraps every response in the envelope
|
|
30
|
+
* `{errno, errMsg, data}`: a result as `{errno: null, errMsg: null, data}` (`data` is `null` when the action returned
|
|
31
|
+
* nothing, and its status is not turned into 204), an error as `{errno, errMsg, data: null}` with the error's status,
|
|
32
|
+
* the `errorStatus` option, or for the errors of the actions the `actionErrorStatus` option.
|
|
33
|
+
* @param options The envelope options.
|
|
34
|
+
* @returns The responder, to give to the `respond` option of `fastifyHTTPEntrypoint`.
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import {Application} from 'lakutata'
|
|
38
|
+
* import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
39
|
+
* import {GET} from 'lakutata/decorator/ctrl'
|
|
40
|
+
* import {fastifyHTTPEntrypoint, fastifyJSONEnvelope} from 'lakutata/adapter/fastify'
|
|
41
|
+
*
|
|
42
|
+
* class PingController extends Controller {
|
|
43
|
+
* //GET /ping answers {"errno": null, "errMsg": null, "data": "pong"}
|
|
44
|
+
* @GET('/ping')
|
|
45
|
+
* public async ping(): Promise<string> {
|
|
46
|
+
* return 'pong'
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* Application.run({
|
|
51
|
+
* id: 'web.app',
|
|
52
|
+
* name: 'Web',
|
|
53
|
+
* components: {
|
|
54
|
+
* entrypoint: buildEntrypoints({
|
|
55
|
+
* controllers: [PingController],
|
|
56
|
+
* //The errors of the actions answered with 200 too (as the applications of lakutata 2.x), the routes not
|
|
57
|
+
* //found and the errors of the hooks with their status (404, 401…)
|
|
58
|
+
* http: fastifyHTTPEntrypoint({port: 8080, respond: fastifyJSONEnvelope({actionErrorStatus: 200})})
|
|
59
|
+
* })
|
|
60
|
+
* }
|
|
61
|
+
* })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function fastifyJSONEnvelope(options = {}) {
|
|
65
|
+
return {
|
|
66
|
+
success: (reply, data) => sendJSON(reply, { errno: null, errMsg: null, data: data === undefined ? null : data }),
|
|
67
|
+
error: (reply, error, _request, origin) => {
|
|
68
|
+
const status = origin === 'action' ? options.actionErrorStatus ?? options.errorStatus : options.errorStatus;
|
|
69
|
+
reply.code(status ?? statusOfError(error));
|
|
70
|
+
sendJSON(reply, { ...describeError(error), data: null });
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { ServiceContext } from '../../context/ServiceContext.js';
|
|
4
|
+
import { ServiceChannel } from '../../channel/BuiltinChannels.js';
|
|
5
|
+
import { ServiceResponseCodec } from '../../lib/ServiceResponseCodec.js';
|
|
6
|
+
import { EntrypointStoppedException } from '../../exceptions/EntrypointStoppedException.js';
|
|
7
|
+
/**
|
|
8
|
+
* Get the HTTP request carrying a service action's request, when {@link httpServiceEntrypoint} serves it (to read its
|
|
9
|
+
* headers, its remote address…).
|
|
10
|
+
* @param context The service action's context (`this.context` in a controller).
|
|
11
|
+
* @returns The Node.js HTTP request, or `undefined` when another adapter serves the request.
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import {Controller, httpRequestOfContext, ServiceContext} from 'lakutata/com/entrypoint'
|
|
15
|
+
* import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
16
|
+
*
|
|
17
|
+
* class WhoAmIController extends Controller {
|
|
18
|
+
* @ServiceAction({cmd: 'whoami'})
|
|
19
|
+
* public async whoami(): Promise<string | undefined> {
|
|
20
|
+
* return httpRequestOfContext(this.context as ServiceContext)?.socket.remoteAddress
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function httpRequestOfContext(context) {
|
|
26
|
+
return context.request;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The headers of the responses: the tools of the browsers may call the adapter (the development of the applications)
|
|
30
|
+
*/
|
|
31
|
+
const CORS_HEADERS = {
|
|
32
|
+
'Access-Control-Allow-Origin': '*',
|
|
33
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
34
|
+
'Access-Control-Allow-Headers': 'Content-Type, id'
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The id of the client of a request: its header id, its query id
|
|
38
|
+
* @param request
|
|
39
|
+
* @param url
|
|
40
|
+
*/
|
|
41
|
+
function clientIdOf(request, url) {
|
|
42
|
+
const given = request.headers.id ?? url.searchParams.get('id');
|
|
43
|
+
const id = Array.isArray(given) ? given[0] : given ?? undefined;
|
|
44
|
+
return id && id.length <= 200 ? id : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Write a response of JSON
|
|
48
|
+
* @param response
|
|
49
|
+
* @param status
|
|
50
|
+
* @param body
|
|
51
|
+
*/
|
|
52
|
+
function reply(response, status, body) {
|
|
53
|
+
if (response.headersSent || response.writableEnded)
|
|
54
|
+
return;
|
|
55
|
+
response.writeHead(status, { ...CORS_HEADERS, 'Content-Type': 'application/json; charset=utf-8' }).end(JSON.stringify(body));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The body of a request, up to a limit
|
|
59
|
+
* @param request
|
|
60
|
+
* @param limit
|
|
61
|
+
*/
|
|
62
|
+
async function readBody(request, limit) {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let size = 0;
|
|
65
|
+
for await (const chunk of request) {
|
|
66
|
+
size += chunk.length;
|
|
67
|
+
if (size > limit)
|
|
68
|
+
throw Object.assign(new Error(`The body of the request is larger than ${limit} bytes`), { statusCode: 413 });
|
|
69
|
+
chunks.push(chunk);
|
|
70
|
+
}
|
|
71
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a service adapter of an entrypoint served over plain HTTP, handy to call the service actions from tools such
|
|
75
|
+
* as curl or Postman during development. It needs no optional dependency.
|
|
76
|
+
*
|
|
77
|
+
* A request is a `POST` to the path whose JSON body holds the action's pattern and input (`{"cmd": "echo", "text":
|
|
78
|
+
* "hi"}`). The response is always 200 with the body `{success, error, payload}` (see `ServiceResponseCodec`): `payload`
|
|
79
|
+
* is the result, or `error` the whole error object when the action failed (no matching action, validation…). A body that
|
|
80
|
+
* is not JSON or exceeds `limit` is answered with 400 and an error body; any other method or path with 404. The CORS
|
|
81
|
+
* headers allow any origin.
|
|
82
|
+
*
|
|
83
|
+
* The events pushed to the clients (`Entrypoint.notify`, `Entrypoint.broadcast`) are streamed as Server-Sent Events by
|
|
84
|
+
* `GET <path>/events`: each event's name is the SSE event, its arguments the JSON array of its data. A client gives its
|
|
85
|
+
* id in the `id` header or the `id` query parameter (at most 200 characters), the same on its requests so that they
|
|
86
|
+
* carry it as `ServiceContext.clientId`; without one, the stream assigns a random id, sent first as the `client` event.
|
|
87
|
+
* The streams receive a keep-alive comment every 15 seconds.
|
|
88
|
+
*
|
|
89
|
+
* When a client disconnects before its response, the action's abort signal fires. When the entrypoint stops, the new
|
|
90
|
+
* requests are answered with an `EntrypointStoppedException` error; once the requests in progress end, the event
|
|
91
|
+
* streams are closed and the server shuts down.
|
|
92
|
+
* @param options The adapter options; by default it listens on port 3000 of all the interfaces, on the path `/`.
|
|
93
|
+
* @returns The adapter, to give to the `service` option of `buildEntrypoints`.
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* import {Application} from 'lakutata'
|
|
97
|
+
* import {buildEntrypoints, Controller, httpServiceEntrypoint} from 'lakutata/com/entrypoint'
|
|
98
|
+
* import {ServiceAction} from 'lakutata/decorator/ctrl'
|
|
99
|
+
*
|
|
100
|
+
* class EchoController extends Controller {
|
|
101
|
+
* @ServiceAction({cmd: 'echo'})
|
|
102
|
+
* public async echo(input: {text: string}): Promise<string> {
|
|
103
|
+
* return input.text
|
|
104
|
+
* }
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* Application.run({
|
|
108
|
+
* id: 'echo.service',
|
|
109
|
+
* name: 'Echo',
|
|
110
|
+
* components: {
|
|
111
|
+
* entrypoint: buildEntrypoints({
|
|
112
|
+
* controllers: [EchoController],
|
|
113
|
+
* service: httpServiceEntrypoint({port: 3000, path: '/rpc'})
|
|
114
|
+
* })
|
|
115
|
+
* }
|
|
116
|
+
* })
|
|
117
|
+
*
|
|
118
|
+
* //A client: the response is {"success": true, "error": null, "payload": "hi"}
|
|
119
|
+
* const response: Response = await fetch('http://localhost:3000/rpc', {
|
|
120
|
+
* method: 'POST',
|
|
121
|
+
* headers: {'Content-Type': 'application/json', 'id': 'client-1'},
|
|
122
|
+
* body: JSON.stringify({cmd: 'echo', text: 'hi'})
|
|
123
|
+
* })
|
|
124
|
+
* console.log(await response.json())
|
|
125
|
+
* //The events pushed to client-1: GET http://localhost:3000/rpc/events?id=client-1 (text/event-stream)
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function httpServiceEntrypoint(options = {}) {
|
|
129
|
+
const base = (options.path ?? '/').replace(/\/+$/, '');
|
|
130
|
+
const requestPath = base || '/';
|
|
131
|
+
const eventsPath = `${base}/events`;
|
|
132
|
+
const limit = options.limit ?? 10 * 1024 * 1024;
|
|
133
|
+
let server;
|
|
134
|
+
let stopping = false;
|
|
135
|
+
let keepAlive;
|
|
136
|
+
//The event streams of each client (a client may listen several times)
|
|
137
|
+
const clients = new Map();
|
|
138
|
+
/**
|
|
139
|
+
* Write an event to a stream
|
|
140
|
+
* @param stream
|
|
141
|
+
* @param event
|
|
142
|
+
* @param args
|
|
143
|
+
*/
|
|
144
|
+
const write = (stream, event, args) => {
|
|
145
|
+
stream.write(`event: ${event.replace(/[\r\n]+/g, ' ')}\ndata: ${JSON.stringify(args)}\n\n`);
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Stream the events of a client
|
|
149
|
+
* @param request
|
|
150
|
+
* @param response
|
|
151
|
+
* @param url
|
|
152
|
+
*/
|
|
153
|
+
const listen = (request, response, url) => {
|
|
154
|
+
const clientId = clientIdOf(request, url) ?? randomUUID();
|
|
155
|
+
response.writeHead(200, { ...CORS_HEADERS, 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
|
156
|
+
//Its id, given to the requests of the client (their header id)
|
|
157
|
+
write(response, 'client', [clientId]);
|
|
158
|
+
if (!clients.has(clientId))
|
|
159
|
+
clients.set(clientId, new Set());
|
|
160
|
+
clients.get(clientId).add(response);
|
|
161
|
+
request.once('close', () => {
|
|
162
|
+
clients.get(clientId)?.delete(response);
|
|
163
|
+
if (!clients.get(clientId)?.size)
|
|
164
|
+
clients.delete(clientId);
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Answer a request of an action
|
|
169
|
+
* @param runtime
|
|
170
|
+
* @param request
|
|
171
|
+
* @param response
|
|
172
|
+
* @param url
|
|
173
|
+
*/
|
|
174
|
+
const invoke = async (runtime, request, response, url) => {
|
|
175
|
+
if (stopping)
|
|
176
|
+
return reply(response, 200, ServiceResponseCodec.encodeError(new EntrypointStoppedException('The entrypoint is stopping')));
|
|
177
|
+
//Aborted when its client leaves before its response
|
|
178
|
+
const aborted = new AbortController();
|
|
179
|
+
response.once('close', () => {
|
|
180
|
+
if (!response.writableFinished)
|
|
181
|
+
aborted.abort();
|
|
182
|
+
});
|
|
183
|
+
let data;
|
|
184
|
+
try {
|
|
185
|
+
const body = await readBody(request, limit);
|
|
186
|
+
data = body ? JSON.parse(body) : {};
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
//413 for a body larger than the limit, 400 for a body which is not JSON
|
|
190
|
+
return reply(response, error.statusCode ?? 400, ServiceResponseCodec.encodeError(error instanceof SyntaxError ? new Error('The body of the request is not JSON') : error));
|
|
191
|
+
}
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
const context = new ServiceContext({ data: data && typeof data === 'object' ? data : {}, request: request, clientId: clientIdOf(request, url) });
|
|
195
|
+
result = ServiceResponseCodec.encode(await runtime.dispatch(context, { signal: aborted.signal }), false);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
result = ServiceResponseCodec.encodeError(error);
|
|
199
|
+
}
|
|
200
|
+
reply(response, 200, result);
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
channel: 'service',
|
|
204
|
+
definition: ServiceChannel,
|
|
205
|
+
name: options.name,
|
|
206
|
+
middlewares: options.middlewares,
|
|
207
|
+
start: async (runtime) => {
|
|
208
|
+
server = createServer((request, response) => {
|
|
209
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
210
|
+
if (request.method === 'OPTIONS')
|
|
211
|
+
return void response.writeHead(204, CORS_HEADERS).end();
|
|
212
|
+
if (request.method === 'GET' && url.pathname === eventsPath)
|
|
213
|
+
return listen(request, response, url);
|
|
214
|
+
if (request.method === 'POST' && url.pathname === requestPath)
|
|
215
|
+
return void invoke(runtime, request, response, url);
|
|
216
|
+
reply(response, 404, { success: false, error: { message: `POST ${requestPath} invokes the actions, GET ${eventsPath} streams their events` }, payload: null });
|
|
217
|
+
});
|
|
218
|
+
//The event streams kept open by the proxies
|
|
219
|
+
keepAlive = setInterval(() => clients.forEach((streams) => streams.forEach((stream) => stream.write(': keep-alive\n\n'))), 15000);
|
|
220
|
+
keepAlive.unref();
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
server.once('error', reject).listen(options.port ?? 3000, options.host, () => {
|
|
223
|
+
server.off('error', reject);
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
await options.listening?.(server);
|
|
228
|
+
},
|
|
229
|
+
stop: () => {
|
|
230
|
+
//The new requests are refused, the requests in progress end
|
|
231
|
+
stopping = true;
|
|
232
|
+
},
|
|
233
|
+
close: async () => {
|
|
234
|
+
clearInterval(keepAlive);
|
|
235
|
+
clients.forEach((streams) => streams.forEach((stream) => stream.end()));
|
|
236
|
+
clients.clear();
|
|
237
|
+
if (!server)
|
|
238
|
+
return;
|
|
239
|
+
const closing = new Promise((resolve) => void server.close(() => resolve()));
|
|
240
|
+
server.closeAllConnections();
|
|
241
|
+
await closing;
|
|
242
|
+
},
|
|
243
|
+
notify: (clientId, event, args) => {
|
|
244
|
+
const streams = clients.get(clientId);
|
|
245
|
+
streams?.forEach((stream) => write(stream, event, args));
|
|
246
|
+
return !!streams?.size;
|
|
247
|
+
},
|
|
248
|
+
broadcast: (event, args) => {
|
|
249
|
+
clients.forEach((streams) => streams.forEach((stream) => write(stream, event, args)));
|
|
250
|
+
return clients.size;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|