@decohere/http 0.0.1-dev.126 → 0.0.1-dev.5
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/README.md +2 -2
- package/package.json +4 -13
- package/src/app/app.ts +147 -0
- package/src/app/module.ts +19 -0
- package/src/auth/ability.ts +56 -0
- package/src/auth/auth.config.ts +34 -0
- package/src/auth/auth.extensions.ts +115 -0
- package/src/auth/auth.types.ts +124 -0
- package/src/auth/casl-authorization.service.ts +62 -0
- package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
- package/src/auth/safe-outbound-url.ts +117 -0
- package/src/auth/token-service.ts +192 -0
- package/src/builder/builder.ts +118 -0
- package/{es6/src/const.js → src/const.ts} +25 -17
- package/src/health/health.controller.ts +34 -0
- package/src/health/health.extensions.ts +53 -0
- package/src/health/readiness.service.ts +33 -0
- package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
- package/src/hosting/hosted-service-host.ts +102 -0
- package/src/hosting/hosted-service.ts +31 -0
- package/src/http/http.compression.ts +38 -0
- package/src/http/http.extensions.ts +244 -0
- package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
- package/src/http/http.options.ts +41 -0
- package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
- package/src/http/http.static.ts +359 -0
- package/{es6/src/index.js → src/index.ts} +1 -0
- package/src/openapi/index.ts +22 -0
- package/src/openapi/openapi.generator.ts +78 -0
- package/src/openapi/openapi.types.ts +60 -0
- package/src/openapi/request-schema.ts +155 -0
- package/src/openapi/request-validator.ts +95 -0
- package/src/providers/fastify.provider.ts +454 -0
- package/src/routing/anvil.types.ts +150 -0
- package/src/routing/decorators/argument.decorator.ts +33 -0
- package/src/routing/decorators/auth.decorator.ts +79 -0
- package/src/routing/decorators/controller.decorator.ts +9 -0
- package/src/routing/decorators/field.decorator.ts +15 -0
- package/src/routing/decorators/request.decorator.ts +34 -0
- package/src/routing/decorators/response.decorator.ts +18 -0
- package/src/routing/decorators/route.decorator.ts +52 -0
- package/src/routing/filters/exception-filter-host.ts +90 -0
- package/src/routing/filters/exception-filter.ts +29 -0
- package/src/routing/middleware/http-access-log.middleware.ts +36 -0
- package/src/routing/middleware/middleware.ts +26 -0
- package/src/routing/middleware/request-id.ts +18 -0
- package/src/routing/path.util.ts +38 -0
- package/src/routing/pipes/pipes.ts +132 -0
- package/src/routing/services/route-argument-resolver.ts +92 -0
- package/src/routing/services/route-registry.ts +229 -0
- package/src/routing/services/router.ts +166 -0
- package/src/sse/sse.decorator.ts +41 -0
- package/src/sse/sse.encoder.ts +86 -0
- package/src/sse/sse.types.ts +47 -0
- package/src/sse/sse.wire.ts +34 -0
- package/src/testing/boot-module.ts +63 -0
- package/src/testing/sse-reader.ts +120 -0
- package/tests/app.hosted-services.spec.ts +292 -0
- package/tests/app.shutdown.integration.spec.ts +160 -0
- package/tests/app.spec.ts +132 -0
- package/tests/auth.ability.spec.ts +48 -0
- package/tests/auth.decorators.spec.ts +20 -0
- package/tests/auth.fail-closed.pentest.spec.ts +77 -0
- package/tests/auth.integration.spec.ts +234 -0
- package/tests/auth.jwks-cache.spec.ts +52 -0
- package/tests/auth.missing-auth.pentest.spec.ts +56 -0
- package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
- package/tests/auth.oidc.integration.spec.ts +161 -0
- package/tests/auth.token-service.spec.ts +251 -0
- package/tests/builder.spec.ts +12 -0
- package/tests/health.integration.spec.ts +111 -0
- package/tests/helpers/listen-ephemeral.ts +13 -0
- package/tests/helpers/local-oidc-server.ts +100 -0
- package/tests/http.access-log.integration.spec.ts +111 -0
- package/tests/http.boolean-query.integration.spec.ts +59 -0
- package/tests/http.compression.integration.spec.ts +604 -0
- package/tests/http.date-query.integration.spec.ts +71 -0
- package/tests/http.global-prefix.integration.spec.ts +284 -0
- package/tests/http.number-query.integration.spec.ts +65 -0
- package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
- package/tests/http.pipeline-order.integration.spec.ts +95 -0
- package/tests/http.request-cleanup.integration.spec.ts +57 -0
- package/tests/http.request-id-scope.pentest.spec.ts +50 -0
- package/tests/http.security.integration.spec.ts +106 -0
- package/tests/http.static.integration.spec.ts +847 -0
- package/tests/http.static.spec.ts +111 -0
- package/tests/http.surface.integration.spec.ts +167 -0
- package/tests/openapi.integration.spec.ts +193 -0
- package/tests/openapi.validator-cache.integration.spec.ts +55 -0
- package/tests/routing.decorators.spec.ts +15 -0
- package/tests/routing.registry.spec.ts +44 -0
- package/tests/routing.resolver.spec.ts +92 -0
- package/tests/sse.integration.spec.ts +421 -0
- package/tests/sse.wire.spec.ts +33 -0
- package/LICENSE +0 -15
- package/commonjs/src/app/app.js +0 -166
- package/commonjs/src/app/index.js +0 -19
- package/commonjs/src/app/module.js +0 -17
- package/commonjs/src/auth/ability.js +0 -54
- package/commonjs/src/auth/auth.config.js +0 -23
- package/commonjs/src/auth/auth.extensions.js +0 -106
- package/commonjs/src/auth/auth.types.js +0 -65
- package/commonjs/src/auth/casl-authorization.service.js +0 -36
- package/commonjs/src/auth/index.js +0 -23
- package/commonjs/src/auth/principal-provider.js +0 -19
- package/commonjs/src/auth/safe-outbound-url.js +0 -102
- package/commonjs/src/auth/token-service.js +0 -151
- package/commonjs/src/builder/builder.js +0 -95
- package/commonjs/src/builder/index.js +0 -18
- package/commonjs/src/const.js +0 -87
- package/commonjs/src/health/health.controller.js +0 -75
- package/commonjs/src/health/health.extensions.js +0 -50
- package/commonjs/src/health/index.js +0 -21
- package/commonjs/src/health/readiness.service.js +0 -67
- package/commonjs/src/health/readiness.types.js +0 -7
- package/commonjs/src/hosting/hosted-service-host.js +0 -88
- package/commonjs/src/hosting/hosted-service.js +0 -11
- package/commonjs/src/hosting/index.js +0 -19
- package/commonjs/src/http/http.compression.js +0 -27
- package/commonjs/src/http/http.extensions.js +0 -269
- package/commonjs/src/http/http.options.js +0 -26
- package/commonjs/src/http/http.options.store.js +0 -20
- package/commonjs/src/http/http.security.js +0 -27
- package/commonjs/src/http/http.static.js +0 -248
- package/commonjs/src/http/index.js +0 -22
- package/commonjs/src/index.js +0 -27
- package/commonjs/src/openapi/index.js +0 -45
- package/commonjs/src/openapi/openapi.generator.js +0 -103
- package/commonjs/src/openapi/openapi.types.js +0 -4
- package/commonjs/src/openapi/request-schema.js +0 -187
- package/commonjs/src/openapi/request-validator.js +0 -85
- package/commonjs/src/providers/fastify.provider.js +0 -391
- package/commonjs/src/providers/index.js +0 -18
- package/commonjs/src/routing/anvil.types.js +0 -99
- package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
- package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
- package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
- package/commonjs/src/routing/decorators/field.decorator.js +0 -25
- package/commonjs/src/routing/decorators/index.js +0 -24
- package/commonjs/src/routing/decorators/request.decorator.js +0 -30
- package/commonjs/src/routing/decorators/response.decorator.js +0 -34
- package/commonjs/src/routing/decorators/route.decorator.js +0 -67
- package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
- package/commonjs/src/routing/filters/exception-filter.js +0 -28
- package/commonjs/src/routing/index.js +0 -25
- package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
- package/commonjs/src/routing/middleware/middleware.js +0 -20
- package/commonjs/src/routing/middleware/request-id.js +0 -30
- package/commonjs/src/routing/path.util.js +0 -51
- package/commonjs/src/routing/pipes/index.js +0 -18
- package/commonjs/src/routing/pipes/pipes.js +0 -145
- package/commonjs/src/routing/services/index.js +0 -18
- package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
- package/commonjs/src/routing/services/route-registry.js +0 -194
- package/commonjs/src/routing/services/router.js +0 -158
- package/commonjs/src/sse/index.js +0 -26
- package/commonjs/src/sse/sse.decorator.js +0 -42
- package/commonjs/src/sse/sse.encoder.js +0 -73
- package/commonjs/src/sse/sse.types.js +0 -36
- package/commonjs/src/sse/sse.wire.js +0 -40
- package/commonjs/src/testing/boot-module.js +0 -38
- package/commonjs/src/testing/index.js +0 -20
- package/commonjs/src/testing/sse-reader.js +0 -98
- package/es6/src/app/app.js +0 -148
- package/es6/src/app/index.js +0 -2
- package/es6/src/app/module.js +0 -7
- package/es6/src/auth/ability.js +0 -45
- package/es6/src/auth/auth.config.js +0 -19
- package/es6/src/auth/auth.extensions.js +0 -83
- package/es6/src/auth/auth.types.js +0 -35
- package/es6/src/auth/casl-authorization.service.js +0 -40
- package/es6/src/auth/index.js +0 -6
- package/es6/src/auth/principal-provider.js +0 -14
- package/es6/src/auth/safe-outbound-url.js +0 -99
- package/es6/src/auth/token-service.js +0 -141
- package/es6/src/builder/builder.js +0 -85
- package/es6/src/builder/index.js +0 -1
- package/es6/src/health/health.controller.js +0 -65
- package/es6/src/health/health.extensions.js +0 -40
- package/es6/src/health/index.js +0 -4
- package/es6/src/health/readiness.service.js +0 -57
- package/es6/src/health/readiness.types.js +0 -4
- package/es6/src/hosting/hosted-service-host.js +0 -81
- package/es6/src/hosting/hosted-service.js +0 -8
- package/es6/src/hosting/index.js +0 -2
- package/es6/src/http/http.compression.js +0 -9
- package/es6/src/http/http.extensions.js +0 -259
- package/es6/src/http/http.options.js +0 -16
- package/es6/src/http/http.options.store.js +0 -13
- package/es6/src/http/http.security.js +0 -9
- package/es6/src/http/http.static.js +0 -224
- package/es6/src/http/index.js +0 -5
- package/es6/src/openapi/index.js +0 -3
- package/es6/src/openapi/openapi.generator.js +0 -82
- package/es6/src/openapi/openapi.types.js +0 -1
- package/es6/src/openapi/request-schema.js +0 -160
- package/es6/src/openapi/request-validator.js +0 -71
- package/es6/src/providers/fastify.provider.js +0 -376
- package/es6/src/providers/index.js +0 -1
- package/es6/src/routing/anvil.types.js +0 -57
- package/es6/src/routing/decorators/argument.decorator.js +0 -28
- package/es6/src/routing/decorators/auth.decorator.js +0 -58
- package/es6/src/routing/decorators/controller.decorator.js +0 -6
- package/es6/src/routing/decorators/field.decorator.js +0 -15
- package/es6/src/routing/decorators/index.js +0 -7
- package/es6/src/routing/decorators/request.decorator.js +0 -24
- package/es6/src/routing/decorators/response.decorator.js +0 -16
- package/es6/src/routing/decorators/route.decorator.js +0 -37
- package/es6/src/routing/filters/exception-filter-host.js +0 -127
- package/es6/src/routing/filters/exception-filter.js +0 -10
- package/es6/src/routing/index.js +0 -8
- package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
- package/es6/src/routing/middleware/middleware.js +0 -10
- package/es6/src/routing/middleware/request-id.js +0 -14
- package/es6/src/routing/path.util.js +0 -33
- package/es6/src/routing/pipes/index.js +0 -1
- package/es6/src/routing/pipes/pipes.js +0 -118
- package/es6/src/routing/services/index.js +0 -1
- package/es6/src/routing/services/route-argument-resolver.js +0 -67
- package/es6/src/routing/services/route-registry.js +0 -184
- package/es6/src/routing/services/router.js +0 -148
- package/es6/src/sse/index.js +0 -2
- package/es6/src/sse/sse.decorator.js +0 -31
- package/es6/src/sse/sse.encoder.js +0 -67
- package/es6/src/sse/sse.types.js +0 -22
- package/es6/src/sse/sse.wire.js +0 -30
- package/es6/src/testing/boot-module.js +0 -35
- package/es6/src/testing/index.js +0 -2
- package/es6/src/testing/sse-reader.js +0 -91
- package/types/src/app/app.d.ts +0 -41
- package/types/src/app/module.d.ts +0 -15
- package/types/src/auth/ability.d.ts +0 -19
- package/types/src/auth/auth.config.d.ts +0 -28
- package/types/src/auth/auth.extensions.d.ts +0 -53
- package/types/src/auth/auth.types.d.ts +0 -73
- package/types/src/auth/casl-authorization.service.d.ts +0 -28
- package/types/src/auth/safe-outbound-url.d.ts +0 -9
- package/types/src/auth/token-service.d.ts +0 -31
- package/types/src/builder/builder.d.ts +0 -36
- package/types/src/const.d.ts +0 -32
- package/types/src/health/health.controller.d.ts +0 -16
- package/types/src/health/health.extensions.d.ts +0 -31
- package/types/src/health/readiness.service.d.ts +0 -9
- package/types/src/hosting/hosted-service-host.d.ts +0 -37
- package/types/src/hosting/hosted-service.d.ts +0 -31
- package/types/src/http/http.compression.d.ts +0 -32
- package/types/src/http/http.extensions.d.ts +0 -79
- package/types/src/http/http.options.d.ts +0 -39
- package/types/src/http/http.static.d.ts +0 -160
- package/types/src/index.d.ts +0 -10
- package/types/src/openapi/index.d.ts +0 -6
- package/types/src/openapi/openapi.generator.d.ts +0 -6
- package/types/src/openapi/openapi.types.d.ts +0 -58
- package/types/src/openapi/request-schema.d.ts +0 -21
- package/types/src/openapi/request-validator.d.ts +0 -9
- package/types/src/providers/fastify.provider.d.ts +0 -56
- package/types/src/routing/anvil.types.d.ts +0 -119
- package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
- package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
- package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
- package/types/src/routing/decorators/field.decorator.d.ts +0 -2
- package/types/src/routing/decorators/request.decorator.d.ts +0 -8
- package/types/src/routing/decorators/response.decorator.d.ts +0 -4
- package/types/src/routing/decorators/route.decorator.d.ts +0 -7
- package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
- package/types/src/routing/filters/exception-filter.d.ts +0 -16
- package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
- package/types/src/routing/middleware/middleware.d.ts +0 -10
- package/types/src/routing/middleware/request-id.d.ts +0 -6
- package/types/src/routing/path.util.d.ts +0 -9
- package/types/src/routing/pipes/pipes.d.ts +0 -21
- package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
- package/types/src/routing/services/route-registry.d.ts +0 -20
- package/types/src/routing/services/router.d.ts +0 -22
- package/types/src/sse/sse.decorator.d.ts +0 -17
- package/types/src/sse/sse.encoder.d.ts +0 -16
- package/types/src/sse/sse.types.d.ts +0 -37
- package/types/src/sse/sse.wire.d.ts +0 -9
- package/types/src/testing/boot-module.d.ts +0 -34
- package/types/src/testing/sse-reader.d.ts +0 -29
- /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
- /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
- /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
- /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
- /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
- /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
- /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
- /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
- /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
- /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
- /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
- /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
- /package/{types/src/testing/index.d.ts → src/testing/index.ts} +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import type { ContainerBuilder } from "@decohere/di";
|
|
3
|
+
import { inject, injectable } from "@decohere/di/decorators";
|
|
4
|
+
import type { IConfigurationService } from "@decohere/core/configuration";
|
|
5
|
+
import { ConfigurationServiceToken } from "@decohere/core/configuration";
|
|
6
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import type { OpenApiDocument } from "../src/openapi/openapi.types.js";
|
|
9
|
+
import { AuthConfig } from "../src/auth/auth.config.js";
|
|
10
|
+
import "../src/auth/auth.extensions.js";
|
|
11
|
+
import { TokenService } from "../src/auth/token-service.js";
|
|
12
|
+
import {
|
|
13
|
+
AllowAnonymous,
|
|
14
|
+
Authorize,
|
|
15
|
+
Controller,
|
|
16
|
+
Get,
|
|
17
|
+
LastEventId,
|
|
18
|
+
Signal,
|
|
19
|
+
Sse,
|
|
20
|
+
type SseStream,
|
|
21
|
+
} from "../src/index.js";
|
|
22
|
+
import { bootModule } from "../src/testing/index.js";
|
|
23
|
+
|
|
24
|
+
/** Records what the producers did, so tests can see that cleanup ran. */
|
|
25
|
+
class Probe {
|
|
26
|
+
public started = 0;
|
|
27
|
+
public timersRunning = 0;
|
|
28
|
+
public signalAborted = false;
|
|
29
|
+
public lastEventId: string | undefined;
|
|
30
|
+
public scopeDisposed = false;
|
|
31
|
+
}
|
|
32
|
+
const probe = new Probe();
|
|
33
|
+
|
|
34
|
+
/** Request-scoped: lives as long as the stream, disposed when it ends. */
|
|
35
|
+
@injectable()
|
|
36
|
+
class RequestResource {
|
|
37
|
+
public constructor() {
|
|
38
|
+
probe.scopeDisposed = false;
|
|
39
|
+
}
|
|
40
|
+
public async [Symbol.asyncDispose]() {
|
|
41
|
+
probe.scopeDisposed = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A producer that owns a timer, like a real subscription would. */
|
|
46
|
+
async function* ticker(signal: AbortSignal, intervalMs: number): AsyncGenerator<string> {
|
|
47
|
+
probe.started += 1;
|
|
48
|
+
probe.timersRunning += 1;
|
|
49
|
+
let tick = 0;
|
|
50
|
+
try {
|
|
51
|
+
while (!signal.aborted) {
|
|
52
|
+
await new Promise<void>((resolve) => {
|
|
53
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
54
|
+
signal.addEventListener(
|
|
55
|
+
"abort",
|
|
56
|
+
() => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
resolve();
|
|
59
|
+
},
|
|
60
|
+
{ once: true },
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
if (!signal.aborted) yield `tick ${++tick}`;
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
probe.timersRunning -= 1;
|
|
67
|
+
probe.signalAborted = signal.aborted;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Controller({ prefix: "/feed" })
|
|
72
|
+
class FeedController {
|
|
73
|
+
public constructor(@inject(RequestResource) private readonly resource: RequestResource) {}
|
|
74
|
+
|
|
75
|
+
@Sse()
|
|
76
|
+
@Get("/items")
|
|
77
|
+
public async *items(): SseStream {
|
|
78
|
+
probe.started += 1;
|
|
79
|
+
yield { id: "1", event: "item", data: { name: "first" } };
|
|
80
|
+
yield { id: "2", event: "item", data: { name: "second" } };
|
|
81
|
+
yield "plain text";
|
|
82
|
+
yield { data: "line one\nline two" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Sse()
|
|
86
|
+
@Get("/resume")
|
|
87
|
+
public async *resume(@LastEventId() lastEventId?: string): SseStream {
|
|
88
|
+
probe.lastEventId = lastEventId;
|
|
89
|
+
const from = lastEventId == null ? 1 : Number(lastEventId) + 1;
|
|
90
|
+
for (const id of [from, from + 1]) {
|
|
91
|
+
yield { id: String(id), data: `event ${id}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Sse({ heartbeatMs: 20 })
|
|
96
|
+
@Get("/quiet")
|
|
97
|
+
public async *quiet(@Signal() signal: AbortSignal): SseStream {
|
|
98
|
+
yield "hello";
|
|
99
|
+
await sleep(100, undefined, { signal });
|
|
100
|
+
yield "world";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Sse({ heartbeatMs: 0 })
|
|
104
|
+
@Get("/live")
|
|
105
|
+
public async *live(@Signal() signal: AbortSignal): SseStream {
|
|
106
|
+
void this.resource;
|
|
107
|
+
yield { id: "0", data: "connected" };
|
|
108
|
+
for await (const message of ticker(signal, 10)) yield message;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Sse()
|
|
112
|
+
@Get("/broken")
|
|
113
|
+
public async *broken(): SseStream {
|
|
114
|
+
yield "before";
|
|
115
|
+
throw new Error("producer exploded");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Sse()
|
|
119
|
+
@Get("/not-a-stream")
|
|
120
|
+
public notAStream() {
|
|
121
|
+
return { oops: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@Get("/plain")
|
|
125
|
+
public plain() {
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe("server-sent events from a controller route (#629)", () => {
|
|
131
|
+
it("writes the handler's events as text/event-stream", async () => {
|
|
132
|
+
await using api = await bootModule({
|
|
133
|
+
controllers: [FeedController],
|
|
134
|
+
services: services(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const read = await api.readEvents("/feed/items", { count: 4 });
|
|
138
|
+
|
|
139
|
+
expect(read.status).toBe(200);
|
|
140
|
+
expect(read.headers.get("content-type")).toContain("text/event-stream");
|
|
141
|
+
expect(read.headers.get("cache-control")).toContain("no-cache");
|
|
142
|
+
expect(read.events).toEqual([
|
|
143
|
+
{ id: "1", event: "item", data: '{"name":"first"}' },
|
|
144
|
+
{ id: "2", event: "item", data: '{"name":"second"}' },
|
|
145
|
+
{ data: "plain text" },
|
|
146
|
+
{ data: "line one\nline two" },
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("ends the response when the handler finishes", async () => {
|
|
151
|
+
await using api = await bootModule({
|
|
152
|
+
controllers: [FeedController],
|
|
153
|
+
services: services(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const read = await api.readEvents("/feed/items", { count: Infinity });
|
|
157
|
+
|
|
158
|
+
expect(read.ended).toBe(true);
|
|
159
|
+
expect(read.events).toHaveLength(4);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("gives the handler the Last-Event-ID the browser sends when it reconnects", async () => {
|
|
163
|
+
await using api = await bootModule({
|
|
164
|
+
controllers: [FeedController],
|
|
165
|
+
services: services(),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const first = await api.readEvents("/feed/resume", { count: 2 });
|
|
169
|
+
expect(first.events.map((e) => e.id)).toEqual(["1", "2"]);
|
|
170
|
+
expect(probe.lastEventId).toBeUndefined();
|
|
171
|
+
|
|
172
|
+
const resumed = await api.readEvents("/feed/resume", {
|
|
173
|
+
count: 2,
|
|
174
|
+
headers: { "Last-Event-ID": "2" },
|
|
175
|
+
});
|
|
176
|
+
expect(probe.lastEventId).toBe("2");
|
|
177
|
+
expect(resumed.events.map((e) => e.id)).toEqual(["3", "4"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("writes a heartbeat comment while the producer is quiet", async () => {
|
|
181
|
+
await using api = await bootModule({
|
|
182
|
+
controllers: [FeedController],
|
|
183
|
+
services: services(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const read = await api.readEvents("/feed/quiet", { count: 2 });
|
|
187
|
+
|
|
188
|
+
expect(read.events.map((e) => e.data)).toEqual(["hello", "world"]);
|
|
189
|
+
expect(read.comments.filter((c) => c === "heartbeat").length).toBeGreaterThanOrEqual(2);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("ends the stream without sending a half-built event when the producer throws", async () => {
|
|
193
|
+
await using api = await bootModule({
|
|
194
|
+
controllers: [FeedController],
|
|
195
|
+
services: services(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const read = await api.readEvents("/feed/broken", { count: Infinity });
|
|
199
|
+
|
|
200
|
+
expect(read.events).toEqual([{ data: "before" }]);
|
|
201
|
+
expect(read.ended).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("answers 500 for an @Sse() method that does not return a stream", async () => {
|
|
205
|
+
await using api = await bootModule({
|
|
206
|
+
controllers: [FeedController],
|
|
207
|
+
services: services(),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const read = await api.readEvents("/feed/not-a-stream");
|
|
211
|
+
|
|
212
|
+
expect(read.status).toBe(500);
|
|
213
|
+
expect(read.events).toEqual([]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("leaves ordinary routes alone", async () => {
|
|
217
|
+
await using api = await bootModule({
|
|
218
|
+
controllers: [FeedController],
|
|
219
|
+
services: services(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const response = await fetch(`${api.baseUrl}/feed/plain`);
|
|
223
|
+
|
|
224
|
+
expect(response.headers.get("content-type")).toContain("application/json");
|
|
225
|
+
expect(await response.json()).toEqual({ ok: true });
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("SSE: client disconnect and app stop", () => {
|
|
230
|
+
it("stops the producer, its timer and the request scope when the client disconnects", async () => {
|
|
231
|
+
await using api = await bootModule({
|
|
232
|
+
controllers: [FeedController],
|
|
233
|
+
services: services(),
|
|
234
|
+
});
|
|
235
|
+
const timersBefore = probe.timersRunning;
|
|
236
|
+
probe.signalAborted = false;
|
|
237
|
+
|
|
238
|
+
const read = await api.readEvents("/feed/live", { count: 2 });
|
|
239
|
+
|
|
240
|
+
expect(read.events.length).toBe(2);
|
|
241
|
+
await waitFor(() => probe.timersRunning === timersBefore);
|
|
242
|
+
expect(probe.signalAborted).toBe(true);
|
|
243
|
+
await waitFor(() => probe.scopeDisposed);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("keeps request-scoped services alive while the stream is running", async () => {
|
|
247
|
+
await using api = await bootModule({
|
|
248
|
+
controllers: [FeedController],
|
|
249
|
+
services: services(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const reading = api.readEvents("/feed/live", { count: Infinity, timeoutMs: 4000 });
|
|
253
|
+
await sleep(80);
|
|
254
|
+
|
|
255
|
+
expect(probe.scopeDisposed).toBe(false);
|
|
256
|
+
await api.stop();
|
|
257
|
+
await reading;
|
|
258
|
+
expect(probe.scopeDisposed).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("closes open streams when the app stops, so stopping does not hang", async () => {
|
|
262
|
+
const api = await bootModule({ controllers: [FeedController], services: services() });
|
|
263
|
+
const timersBefore = probe.timersRunning;
|
|
264
|
+
|
|
265
|
+
const reading = api.readEvents("/feed/live", { count: Infinity, timeoutMs: 4000 });
|
|
266
|
+
await waitFor(() => probe.timersRunning > timersBefore);
|
|
267
|
+
|
|
268
|
+
await api.stop();
|
|
269
|
+
const read = await reading;
|
|
270
|
+
|
|
271
|
+
expect(read.ended).toBe(true);
|
|
272
|
+
expect(probe.timersRunning).toBe(timersBefore);
|
|
273
|
+
expect(probe.signalAborted).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("closes open streams on the dispose path Flux uses for a reboot", async () => {
|
|
277
|
+
const api = await bootModule({ controllers: [FeedController], services: services() });
|
|
278
|
+
const timersBefore = probe.timersRunning;
|
|
279
|
+
const reading = api.readEvents("/feed/live", { count: Infinity, timeoutMs: 4000 });
|
|
280
|
+
await waitFor(() => probe.timersRunning > timersBefore);
|
|
281
|
+
|
|
282
|
+
// Flux calls Symbol.asyncDispose on the entry's default export, which is the app.
|
|
283
|
+
await api.app[Symbol.asyncDispose]();
|
|
284
|
+
|
|
285
|
+
expect((await reading).ended).toBe(true);
|
|
286
|
+
expect(probe.timersRunning).toBe(timersBefore);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Auth: a real shared-secret token service, no dev shortcuts.
|
|
291
|
+
const secretConfig = {
|
|
292
|
+
secret: "super-secret-key-that-is-long-enough-for-hs256",
|
|
293
|
+
issuer: "test-issuer",
|
|
294
|
+
audience: "test-audience",
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
class SecretConfigService implements IConfigurationService {
|
|
298
|
+
public get dataSources() {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
public async get<T extends object>(type: Constructable<T> | string): Promise<T> {
|
|
302
|
+
if (type === AuthConfig)
|
|
303
|
+
return Object.assign(new AuthConfig(), secretConfig) as unknown as T;
|
|
304
|
+
return {} as T;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@Controller({ prefix: "/secure" })
|
|
309
|
+
class SecureFeedController {
|
|
310
|
+
@Authorize("read", "Article")
|
|
311
|
+
@Sse()
|
|
312
|
+
@Get("/articles")
|
|
313
|
+
public async *articles(): SseStream {
|
|
314
|
+
probe.started += 1;
|
|
315
|
+
yield { id: "1", data: "secret article" };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@AllowAnonymous()
|
|
319
|
+
@Sse()
|
|
320
|
+
@Get("/public")
|
|
321
|
+
public async *open(): SseStream {
|
|
322
|
+
yield "hello everyone";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
describe("SSE: auth runs before any event is sent", () => {
|
|
327
|
+
const boot = () =>
|
|
328
|
+
bootModule({
|
|
329
|
+
controllers: [SecureFeedController],
|
|
330
|
+
services: (c) =>
|
|
331
|
+
c.registerSingleton(ConfigurationServiceToken, new SecretConfigService()),
|
|
332
|
+
configure: (builder) => builder.withAuth(),
|
|
333
|
+
});
|
|
334
|
+
const tokens = new TokenService(secretConfig);
|
|
335
|
+
|
|
336
|
+
it("answers 401 with no events when there is no token", async () => {
|
|
337
|
+
await using api = await boot();
|
|
338
|
+
probe.started = 0;
|
|
339
|
+
|
|
340
|
+
const read = await api.readEvents("/secure/articles");
|
|
341
|
+
|
|
342
|
+
expect(read.status).toBe(401);
|
|
343
|
+
expect(read.events).toEqual([]);
|
|
344
|
+
expect(read.headers.get("content-type")).not.toContain("text/event-stream");
|
|
345
|
+
expect(probe.started).toBe(0);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("answers 403 with no events when the token lacks the permission", async () => {
|
|
349
|
+
await using api = await boot();
|
|
350
|
+
probe.started = 0;
|
|
351
|
+
const token = await tokens.sign({ sub: "alice", scope: "read:Comment" });
|
|
352
|
+
|
|
353
|
+
const read = await api.readEvents("/secure/articles", {
|
|
354
|
+
headers: { authorization: `Bearer ${token}` },
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(read.status).toBe(403);
|
|
358
|
+
expect(read.events).toEqual([]);
|
|
359
|
+
expect(probe.started).toBe(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("streams to a caller whose token allows it", async () => {
|
|
363
|
+
await using api = await boot();
|
|
364
|
+
const token = await tokens.sign({ sub: "alice", scope: "read:Article" });
|
|
365
|
+
|
|
366
|
+
const read = await api.readEvents("/secure/articles", {
|
|
367
|
+
headers: { authorization: `Bearer ${token}` },
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(read.status).toBe(200);
|
|
371
|
+
expect(read.events).toEqual([{ id: "1", data: "secret article" }]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("honours @AllowAnonymous on a stream", async () => {
|
|
375
|
+
await using api = await boot();
|
|
376
|
+
|
|
377
|
+
const read = await api.readEvents("/secure/public");
|
|
378
|
+
|
|
379
|
+
expect(read.events).toEqual([{ data: "hello everyone" }]);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("SSE: OpenAPI", () => {
|
|
384
|
+
it("documents a stream route as text/event-stream, with the Last-Event-ID header", async () => {
|
|
385
|
+
await using api = await bootModule({
|
|
386
|
+
controllers: [FeedController],
|
|
387
|
+
services: services(),
|
|
388
|
+
configure: (builder) => builder.withOpenApi({ info: { title: "t", version: "1" } }),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const document = (await (
|
|
392
|
+
await fetch(`${api.baseUrl}/openapi.json`)
|
|
393
|
+
).json()) as OpenApiDocument;
|
|
394
|
+
|
|
395
|
+
const stream = document.paths["/feed/items"].get;
|
|
396
|
+
expect(stream.responses["200"].content).toEqual({
|
|
397
|
+
"text/event-stream": { schema: { type: "string" } },
|
|
398
|
+
});
|
|
399
|
+
const resume = document.paths["/feed/resume"].get;
|
|
400
|
+
expect(resume.parameters).toContainEqual({
|
|
401
|
+
name: "Last-Event-ID",
|
|
402
|
+
in: "header",
|
|
403
|
+
schema: { type: "string" },
|
|
404
|
+
});
|
|
405
|
+
expect(document.paths["/feed/plain"].get.responses["200"].content).toBeUndefined();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
function services() {
|
|
410
|
+
return (c: ContainerBuilder) => {
|
|
411
|
+
c.registerRequestScoped(RequestResource);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function waitFor(condition: () => boolean, timeoutMs = 2000) {
|
|
416
|
+
const deadline = Date.now() + timeoutMs;
|
|
417
|
+
while (!condition()) {
|
|
418
|
+
if (Date.now() > deadline) throw new Error("condition not met in time");
|
|
419
|
+
await sleep(5);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { sseWire } from "../src/sse/sse.wire.js";
|
|
3
|
+
|
|
4
|
+
// Edges the HTTP tests cannot see: what the bytes look like for hostile or odd input.
|
|
5
|
+
describe("SSE wire format", () => {
|
|
6
|
+
it("cannot be made to inject a field through a line break in id or event", () => {
|
|
7
|
+
const frame = sseWire.formatMessage({
|
|
8
|
+
id: "1\ndata: injected",
|
|
9
|
+
event: "a\r\nretry: 1",
|
|
10
|
+
data: "x",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(frame).toBe("id: 1 data: injected\nevent: a retry: 1\ndata: x\n\n");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("splits multi-line data into one data field per line, whatever the line ending", () => {
|
|
17
|
+
expect(sseWire.formatMessage("a\r\nb\rc\nd")).toBe(
|
|
18
|
+
"data: a\ndata: b\ndata: c\ndata: d\n\n",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("writes non-string data as JSON and a bare string as-is", () => {
|
|
23
|
+
expect(sseWire.formatMessage({ data: { n: 1 } })).toBe('data: {"n":1}\n\n');
|
|
24
|
+
expect(sseWire.formatMessage("hi")).toBe("data: hi\n\n");
|
|
25
|
+
expect(sseWire.formatMessage(42)).toBe("data: 42\n\n");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("writes retry as a whole number of milliseconds", () => {
|
|
29
|
+
expect(sseWire.formatMessage({ data: "x", retry: 1500.9 })).toBe(
|
|
30
|
+
"retry: 1500\ndata: x\n\n",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/LICENSE
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
ISC License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025-2026 Typesmiths
|
|
4
|
-
|
|
5
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
-
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
-
copyright notice and this permission notice appear in all copies.
|
|
8
|
-
|
|
9
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
-
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
-
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
-
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
-
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
-
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
-
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/commonjs/src/app/app.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
function _export(target, all) {
|
|
6
|
-
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
_export(exports, {
|
|
12
|
-
get ApiConfiguration () {
|
|
13
|
-
return ApiConfiguration;
|
|
14
|
-
},
|
|
15
|
-
get App () {
|
|
16
|
-
return App;
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
const _crucible = require("@decohere/crucible");
|
|
20
|
-
const _decorators = require("@decohere/crucible/decorators");
|
|
21
|
-
const _configuration = require("@decohere/core/configuration");
|
|
22
|
-
const _scribe = require("@decohere/scribe");
|
|
23
|
-
const _const = require("../const.js");
|
|
24
|
-
function _ts_decorate(decorators, target, key, desc) {
|
|
25
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
26
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
27
|
-
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
28
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
29
|
-
}
|
|
30
|
-
function _ts_metadata(k, v) {
|
|
31
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
32
|
-
}
|
|
33
|
-
function _ts_param(paramIndex, decorator) {
|
|
34
|
-
return function(target, key) {
|
|
35
|
-
decorator(target, key, paramIndex);
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
const DEFAULT_PORT = 3000;
|
|
39
|
-
const DEFAULT_SHUTDOWN_SIGNALS = [
|
|
40
|
-
"SIGTERM",
|
|
41
|
-
"SIGINT"
|
|
42
|
-
];
|
|
43
|
-
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
44
|
-
class ApiConfiguration {
|
|
45
|
-
port;
|
|
46
|
-
}
|
|
47
|
-
class App {
|
|
48
|
-
container;
|
|
49
|
-
configurationService;
|
|
50
|
-
httpProvider;
|
|
51
|
-
hostedServices;
|
|
52
|
-
_addresses = Array.of();
|
|
53
|
-
logger = _scribe.scribe.logger(App.name);
|
|
54
|
-
disposing;
|
|
55
|
-
disposingContainer = false;
|
|
56
|
-
signalHandler;
|
|
57
|
-
shutdownSignals = [];
|
|
58
|
-
constructor(container, configurationService, httpProvider, hostedServices){
|
|
59
|
-
this.container = container;
|
|
60
|
-
this.configurationService = configurationService;
|
|
61
|
-
this.httpProvider = httpProvider;
|
|
62
|
-
this.hostedServices = hostedServices;
|
|
63
|
-
}
|
|
64
|
-
get addresses() {
|
|
65
|
-
return this._addresses;
|
|
66
|
-
}
|
|
67
|
-
async start(options) {
|
|
68
|
-
this.logger.info("Starting app");
|
|
69
|
-
// Before HTTP, so a request never reaches a service that has not started.
|
|
70
|
-
await this.hostedServices?.start(this.container);
|
|
71
|
-
// The app was stopped while a hosted service was still starting.
|
|
72
|
-
if (this.disposing != null) return;
|
|
73
|
-
const hasHostedServices = this.hostedServices != null && !this.hostedServices.isEmpty;
|
|
74
|
-
if (!this.httpProvider) {
|
|
75
|
-
if (hasHostedServices) this.stopOnSignals(options);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const port = await this.getPort(options);
|
|
79
|
-
this.logger.info(`Starting http on port ${port}`);
|
|
80
|
-
const startResponse = await this.httpProvider?.start({
|
|
81
|
-
port,
|
|
82
|
-
...options?.host != null ? {
|
|
83
|
-
host: options.host
|
|
84
|
-
} : {}
|
|
85
|
-
});
|
|
86
|
-
this._addresses = [
|
|
87
|
-
...startResponse.addresses
|
|
88
|
-
];
|
|
89
|
-
this.logger.info("Listening on:", this._addresses);
|
|
90
|
-
this.stopOnSignals(options);
|
|
91
|
-
return startResponse;
|
|
92
|
-
}
|
|
93
|
-
[Symbol.asyncDispose]() {
|
|
94
|
-
// The container holds this App too; when stop() disposes the container it lands
|
|
95
|
-
// here, and awaiting our own shutdown would deadlock.
|
|
96
|
-
if (this.disposingContainer) return Promise.resolve();
|
|
97
|
-
return this.dispose();
|
|
98
|
-
}
|
|
99
|
-
/** Stop the app. Calling it again (or from a signal) returns the same shutdown. */ dispose() {
|
|
100
|
-
this.disposing ??= this.stop();
|
|
101
|
-
return this.disposing;
|
|
102
|
-
}
|
|
103
|
-
async stop() {
|
|
104
|
-
// Removed first, so a second Ctrl+C / SIGTERM gets Node's default: exit now.
|
|
105
|
-
this.removeSignalHandlers();
|
|
106
|
-
this.logger.info("Stopping");
|
|
107
|
-
// Hosted services hear the signal at once, so background work winds down while HTTP drains.
|
|
108
|
-
this.hostedServices?.abort();
|
|
109
|
-
try {
|
|
110
|
-
// HTTP first: in-flight requests still need the services (DB pools etc.) that
|
|
111
|
-
// disposing the container would close.
|
|
112
|
-
await this.httpProvider?.stop();
|
|
113
|
-
// Hosted services next, while what they use is still open.
|
|
114
|
-
await this.hostedServices?.stop();
|
|
115
|
-
} finally{
|
|
116
|
-
this.disposingContainer = true;
|
|
117
|
-
await this.container[Symbol.asyncDispose]();
|
|
118
|
-
await _scribe.scribe.stop();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
stopOnSignals(options) {
|
|
122
|
-
const signals = options?.shutdownSignals ?? DEFAULT_SHUTDOWN_SIGNALS;
|
|
123
|
-
const timeoutMs = options?.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
124
|
-
if (signals === false || signals.length === 0) return;
|
|
125
|
-
this.signalHandler = (signal)=>{
|
|
126
|
-
this.logger.info(`Received ${signal}; finishing in-flight requests`);
|
|
127
|
-
// console.error, not the logger: scribe may be the thing that's stuck.
|
|
128
|
-
const timer = setTimeout(()=>{
|
|
129
|
-
console.error(`Shutdown did not finish within ${timeoutMs}ms; exiting.`);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}, timeoutMs);
|
|
132
|
-
timer.unref();
|
|
133
|
-
this.dispose().then(()=>clearTimeout(timer), (error)=>{
|
|
134
|
-
clearTimeout(timer);
|
|
135
|
-
console.error("Shutdown failed:", error);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
});
|
|
138
|
-
};
|
|
139
|
-
this.shutdownSignals = signals;
|
|
140
|
-
for (const signal of signals)process.on(signal, this.signalHandler);
|
|
141
|
-
}
|
|
142
|
-
removeSignalHandlers() {
|
|
143
|
-
if (!this.signalHandler) return;
|
|
144
|
-
for (const signal of this.shutdownSignals)process.off(signal, this.signalHandler);
|
|
145
|
-
}
|
|
146
|
-
async getPort(options) {
|
|
147
|
-
const port = options?.port ?? (await this.configurationService?.get(ApiConfiguration))?.port ?? DEFAULT_PORT;
|
|
148
|
-
return port;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
App = _ts_decorate([
|
|
152
|
-
_ts_param(0, (0, _decorators.inject)(_crucible.LifetimeScopeToken)),
|
|
153
|
-
_ts_param(1, (0, _decorators.inject)(_configuration.ConfigurationServiceToken)),
|
|
154
|
-
_ts_param(1, (0, _decorators.optional)()),
|
|
155
|
-
_ts_param(2, (0, _decorators.inject)(_const.HttpProviderToken)),
|
|
156
|
-
_ts_param(2, (0, _decorators.optional)()),
|
|
157
|
-
_ts_param(3, (0, _decorators.inject)(_const.HostedServiceHostToken)),
|
|
158
|
-
_ts_param(3, (0, _decorators.optional)()),
|
|
159
|
-
_ts_metadata("design:type", Function),
|
|
160
|
-
_ts_metadata("design:paramtypes", [
|
|
161
|
-
typeof LifetimeScope === "undefined" ? Object : LifetimeScope,
|
|
162
|
-
typeof IConfigurationService === "undefined" ? Object : IConfigurationService,
|
|
163
|
-
typeof HttpProvider === "undefined" ? Object : HttpProvider,
|
|
164
|
-
typeof HostedServiceHost === "undefined" ? Object : HostedServiceHost
|
|
165
|
-
])
|
|
166
|
-
], App);
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
_export_star(require("./app.js"), exports);
|
|
6
|
-
_export_star(require("./module.js"), exports);
|
|
7
|
-
function _export_star(from, to) {
|
|
8
|
-
Object.keys(from).forEach(function(k) {
|
|
9
|
-
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
10
|
-
Object.defineProperty(to, k, {
|
|
11
|
-
enumerable: true,
|
|
12
|
-
get: function() {
|
|
13
|
-
return from[k];
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
return from;
|
|
19
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
value: true
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "Module", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return Module;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
|
-
class Module {
|
|
12
|
-
constructor(data){
|
|
13
|
-
Object.assign(this, data);
|
|
14
|
-
}
|
|
15
|
-
entities;
|
|
16
|
-
repositories;
|
|
17
|
-
}
|