@warlock.js/core 4.15.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -2
- package/bin/warlock.js +154 -1
- package/esm/application/application.d.mts +65 -1
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +62 -0
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -1
- package/esm/cli/cli-commands.manager.mjs +60 -10
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -0
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +40 -0
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/index.mjs +4 -2
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/types.d.mts +10 -2
- package/esm/commands/types.d.mts.map +1 -1
- package/esm/config/config-handlers.d.mts +1 -2
- package/esm/config/config-handlers.d.mts.map +1 -1
- package/esm/config/config-handlers.mjs +3 -2
- package/esm/config/config-handlers.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +29 -0
- package/esm/config/locale-configuration.mjs.map +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts +7 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.mjs +44 -0
- package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -0
- package/esm/connectors/assert-unique-connector-names.d.mts +7 -0
- package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -0
- package/esm/connectors/assert-unique-connector-names.mjs +20 -0
- package/esm/connectors/assert-unique-connector-names.mjs.map +1 -0
- package/esm/connectors/connectors-manager.d.mts +38 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +45 -12
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/describe-server-address.mjs +75 -0
- package/esm/connectors/describe-server-address.mjs.map +1 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +6 -3
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/index.d.mts +4 -1
- package/esm/connectors/index.mjs +4 -1
- package/esm/connectors/register-configured-connectors.d.mts +14 -0
- package/esm/connectors/register-configured-connectors.d.mts.map +1 -0
- package/esm/connectors/register-configured-connectors.mjs +99 -0
- package/esm/connectors/register-configured-connectors.mjs.map +1 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +4 -1
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +76 -1
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +50 -2
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/utils.d.mts +2 -2
- package/esm/database/utils.d.mts.map +1 -1
- package/esm/dev-server/development-server.mjs +1 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs +2 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/read-config-ast.mjs +48 -0
- package/esm/dev-server/read-config-ast.mjs.map +1 -0
- package/esm/dev-server/run-typings-generation.mjs +18 -0
- package/esm/dev-server/run-typings-generation.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +30 -39
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +169 -1
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/stubs.mjs +93 -1
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/context/request-context.d.mts +14 -3
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs +11 -1
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/context/request-memo.d.mts +21 -0
- package/esm/http/context/request-memo.d.mts.map +1 -0
- package/esm/http/context/request-memo.mjs +48 -0
- package/esm/http/context/request-memo.mjs.map +1 -0
- package/esm/http/database/RequestLog.d.mts +164 -2
- package/esm/http/database/RequestLog.d.mts.map +1 -1
- package/esm/http/index.d.mts +6 -5
- package/esm/http/index.mjs +2 -1
- package/esm/http/middleware/cache-response-middleware.d.mts +2 -2
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/index.d.mts +1 -1
- package/esm/http/middleware/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -8
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +9 -22
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.d.mts +6 -3
- package/esm/http/middleware/ip-filter.middleware.d.mts.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +7 -4
- package/esm/http/middleware/ip-filter.middleware.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.d.mts +0 -14
- package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +19 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/request.d.mts +149 -44
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +132 -43
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.mjs +1 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.d.mts.map +1 -1
- package/esm/http/server.mjs.map +1 -1
- package/esm/http/types.d.mts +58 -1
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploaded-file.d.mts +51 -19
- package/esm/http/uploaded-file.d.mts.map +1 -1
- package/esm/http/uploaded-file.mjs +36 -8
- package/esm/http/uploaded-file.mjs.map +1 -1
- package/esm/index.d.mts +17 -9
- package/esm/index.mjs +10 -4
- package/esm/production/build-contributions.mjs +115 -0
- package/esm/production/build-contributions.mjs.map +1 -0
- package/esm/production/production-builder.mjs +97 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +25 -0
- package/esm/production/resolve-build-config.d.mts.map +1 -0
- package/esm/production/resolve-build-config.mjs +43 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +20 -9
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/restful/restful.d.mts +29 -8
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +31 -13
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/describe-route-for-log.mjs +29 -0
- package/esm/router/describe-route-for-log.mjs.map +1 -0
- package/esm/router/index.d.mts +3 -1
- package/esm/router/index.mjs +2 -0
- package/esm/router/log-request-lifecycle.mjs +38 -0
- package/esm/router/log-request-lifecycle.mjs.map +1 -0
- package/esm/router/positional-handler-diagnostics.d.mts +90 -0
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -0
- package/esm/router/positional-handler-diagnostics.mjs +247 -0
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -0
- package/esm/router/route-registry.d.mts +34 -0
- package/esm/router/route-registry.d.mts.map +1 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +87 -63
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +23 -5
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +1 -1
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts +11 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +51 -4
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/vitest-setup.mjs +1 -1
- package/esm/utils/load-environment.mjs +33 -2
- package/esm/utils/load-environment.mjs.map +1 -1
- package/esm/utils/types.d.mts +4 -0
- package/esm/utils/types.d.mts.map +1 -1
- package/esm/validation/validateAll.mjs +4 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/validation/validators/file-validator.d.mts +25 -1
- package/esm/validation/validators/file-validator.d.mts.map +1 -1
- package/esm/warlock-config/index.mjs +1 -1
- package/esm/warlock-config/normalize-build-config.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +36 -2
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +179 -23
- package/llms.txt +1 -1
- package/package.json +23 -20
- package/skills/request-memo/SKILL.md +142 -0
- package/skills/use-middleware/SKILL.md +12 -1
- package/skills/use-request-locals/SKILL.md +144 -0
|
@@ -6,10 +6,40 @@ declare class ConnectorsManager {
|
|
|
6
6
|
* Connectors list
|
|
7
7
|
*/
|
|
8
8
|
private readonly connectors;
|
|
9
|
+
/**
|
|
10
|
+
* The names the framework's own connectors hold.
|
|
11
|
+
*
|
|
12
|
+
* Filled by the constructor's own registration calls rather than declared
|
|
13
|
+
* beside them, so there is exactly ONE place a built-in name exists: the
|
|
14
|
+
* connector that owns it. A second list — in the builder, in a config
|
|
15
|
+
* validator, in a test double — is a list that drifts the moment a built-in
|
|
16
|
+
* is added or renamed, and it drifts silently, because nothing compares the
|
|
17
|
+
* two. Both halves of a connector's life read this one through
|
|
18
|
+
* {@link isBuiltInName}: the build refuses a `connectors` array that claims
|
|
19
|
+
* a built-in name, and so does registration.
|
|
20
|
+
*/
|
|
21
|
+
private readonly builtInNames;
|
|
9
22
|
/**
|
|
10
23
|
* Constructor
|
|
11
24
|
*/
|
|
12
25
|
constructor();
|
|
26
|
+
/**
|
|
27
|
+
* Register a framework-owned connector and reserve its name.
|
|
28
|
+
*
|
|
29
|
+
* Private on purpose: an app connector must not be able to reserve a name,
|
|
30
|
+
* and the set is only correct if every entry in it came from a connector
|
|
31
|
+
* this class constructed itself.
|
|
32
|
+
*/
|
|
33
|
+
private registerBuiltIn;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the given name belongs to a framework built-in connector.
|
|
36
|
+
*
|
|
37
|
+
* Reserved, not merely taken: a configured connector carrying one of these
|
|
38
|
+
* names would be skipped by {@link has} at boot while the build had already
|
|
39
|
+
* drained its contribution, so the artifact would carry a connector the
|
|
40
|
+
* server never runs. Callers refuse the name instead of skipping it.
|
|
41
|
+
*/
|
|
42
|
+
isBuiltInName(name: string): boolean;
|
|
13
43
|
/**
|
|
14
44
|
* Register a connector
|
|
15
45
|
*/
|
|
@@ -18,6 +48,14 @@ declare class ConnectorsManager {
|
|
|
18
48
|
* Get all connectors
|
|
19
49
|
*/
|
|
20
50
|
list(): Connector[];
|
|
51
|
+
/**
|
|
52
|
+
* Whether a connector with the given name is already registered.
|
|
53
|
+
*
|
|
54
|
+
* Names are the identity of a connector here, which is what lets a caller
|
|
55
|
+
* that may run more than once per process register idempotently instead of
|
|
56
|
+
* stacking duplicates that would each boot and shut down.
|
|
57
|
+
*/
|
|
58
|
+
has(name: ConnectorName): boolean;
|
|
21
59
|
/**
|
|
22
60
|
* start all connectors
|
|
23
61
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectors-manager.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/connectors-manager.ts"],"mappings":";;;cAgBa,iBAAA;EAAA;;;EAAA,iBAIM,UAAA;
|
|
1
|
+
{"version":3,"file":"connectors-manager.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/connectors-manager.ts"],"mappings":";;;cAgBa,iBAAA;EAAA;;;EAAA,iBAIM,UAAA;EAwEF;;;;;;;;;;;;EAAA,iBA1DE,YAAA;;;;;EAiDV;;;;;;;EAAA,QAvBC,eAAA;EAkDK;;;;;;;;EAlCN,aAAA,CAAc,IAAA;EA0EyB;;;EAnEvC,QAAA,IAAY,UAAA,EAAY,SAAA;EA8EN;;;EArElB,IAAA,IAAQ,SAAA;EAmJJ;;;;AAA2C;;;EAxI/C,GAAA,CAAI,IAAA,EAAM,aAAA;;;;EAOJ,KAAA,CAAM,eAAA,GAAkB,aAAA,KAAkB,OAAA;;;;;;;;;;EAuB1C,UAAA,CAAW,KAAA,EAAO,uBAAA,GAA0B,OAAA;;;;EAiB5C,YAAA,CAAa,kBAAA,EAAoB,aAAA,KAAkB,OAAA;;;;EAWnD,QAAA,IAAY,OAAA;;;;;;;;;;;;;EAqDlB,qBAAA;AAAA;AAAA,cAyBI,iBAAA,EAAiB,iBAA0B"}
|
|
@@ -3,8 +3,8 @@ import "../application/index.mjs";
|
|
|
3
3
|
import { AccessConnector } from "./access-connector.mjs";
|
|
4
4
|
import { AiConnector } from "./ai-connector.mjs";
|
|
5
5
|
import { CacheConnector } from "./cache-connector.mjs";
|
|
6
|
-
import { HeraldConnector } from "./herald-connector.mjs";
|
|
7
6
|
import { DatabaseConnector } from "./database-connector.mjs";
|
|
7
|
+
import { HeraldConnector } from "./herald-connector.mjs";
|
|
8
8
|
import { HttpConnector } from "./http-connector.mjs";
|
|
9
9
|
import { LoggerConnector } from "./logger-connector.mjs";
|
|
10
10
|
import { MailerConnector } from "./mail-connector.mjs";
|
|
@@ -20,17 +20,40 @@ var ConnectorsManager = class {
|
|
|
20
20
|
*/
|
|
21
21
|
constructor() {
|
|
22
22
|
this.connectors = [];
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
23
|
+
this.builtInNames = /* @__PURE__ */ new Set();
|
|
24
|
+
this.registerBuiltIn(new LoggerConnector());
|
|
25
|
+
this.registerBuiltIn(new MailerConnector());
|
|
26
|
+
this.registerBuiltIn(new HttpConnector());
|
|
27
|
+
this.registerBuiltIn(new DatabaseConnector());
|
|
28
|
+
this.registerBuiltIn(new HeraldConnector());
|
|
29
|
+
this.registerBuiltIn(new CacheConnector());
|
|
30
|
+
this.registerBuiltIn(new StorageConnector());
|
|
31
|
+
this.registerBuiltIn(new SocketConnector());
|
|
32
|
+
this.registerBuiltIn(new NotificationsConnector());
|
|
33
|
+
this.registerBuiltIn(new AccessConnector());
|
|
34
|
+
this.registerBuiltIn(new AiConnector());
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register a framework-owned connector and reserve its name.
|
|
38
|
+
*
|
|
39
|
+
* Private on purpose: an app connector must not be able to reserve a name,
|
|
40
|
+
* and the set is only correct if every entry in it came from a connector
|
|
41
|
+
* this class constructed itself.
|
|
42
|
+
*/
|
|
43
|
+
registerBuiltIn(...connectors) {
|
|
44
|
+
for (const connector of connectors) this.builtInNames.add(connector.name);
|
|
45
|
+
this.register(...connectors);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether the given name belongs to a framework built-in connector.
|
|
49
|
+
*
|
|
50
|
+
* Reserved, not merely taken: a configured connector carrying one of these
|
|
51
|
+
* names would be skipped by {@link has} at boot while the build had already
|
|
52
|
+
* drained its contribution, so the artifact would carry a connector the
|
|
53
|
+
* server never runs. Callers refuse the name instead of skipping it.
|
|
54
|
+
*/
|
|
55
|
+
isBuiltInName(name) {
|
|
56
|
+
return this.builtInNames.has(name);
|
|
34
57
|
}
|
|
35
58
|
/**
|
|
36
59
|
* Register a connector
|
|
@@ -46,6 +69,16 @@ var ConnectorsManager = class {
|
|
|
46
69
|
return this.connectors;
|
|
47
70
|
}
|
|
48
71
|
/**
|
|
72
|
+
* Whether a connector with the given name is already registered.
|
|
73
|
+
*
|
|
74
|
+
* Names are the identity of a connector here, which is what lets a caller
|
|
75
|
+
* that may run more than once per process register idempotently instead of
|
|
76
|
+
* stacking duplicates that would each boot and shut down.
|
|
77
|
+
*/
|
|
78
|
+
has(name) {
|
|
79
|
+
return this.connectors.some((connector) => connector.name === name);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
49
82
|
* start all connectors
|
|
50
83
|
*/
|
|
51
84
|
async start(connectorsNames) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectors-manager.mjs","names":[],"sources":["../../../../../../../core/src/connectors/connectors-manager.ts"],"sourcesContent":["import { Application } from \"../application\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AccessConnector } from \"./access-connector\";\r\nimport { AiConnector } from \"./ai-connector\";\r\nimport { CacheConnector } from \"./cache-connector\";\r\nimport { DatabaseConnector } from \"./database-connector\";\r\nimport { HeraldConnector } from \"./herald-connector\";\r\nimport { HttpConnector } from \"./http-connector\";\r\nimport { LoggerConnector } from \"./logger-connector\";\r\nimport { MailerConnector } from \"./mail-connector\";\r\nimport { NotificationsConnector } from \"./notifications-connector\";\r\nimport { SocketConnector } from \"./socket-connector\";\r\nimport { StorageConnector } from \"./storage.connector\";\r\nimport { ConnectorLifecyclePhase } from \"./types\";\r\nimport type { Connector, ConnectorName } from \"./types\";\r\n\r\nexport class ConnectorsManager {\r\n /**\r\n * Connectors list\r\n */\r\n private readonly connectors: Connector[] = [];\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor() {\r\n this.register(new LoggerConnector());\r\n this.register(new MailerConnector());\r\n this.register(new HttpConnector());\r\n this.register(new DatabaseConnector());\r\n this.register(new HeraldConnector());\r\n this.register(new CacheConnector());\r\n this.register(new StorageConnector());\r\n this.register(new SocketConnector());\r\n this.register(new NotificationsConnector());\r\n this.register(new AccessConnector());\r\n this.register(new AiConnector());\r\n }\r\n\r\n /**\r\n * Register a connector\r\n */\r\n public register(...connectors: Connector[]): void {\r\n this.connectors.push(...connectors);\r\n // sort connectors by priority\r\n this.connectors.sort((a, b) => a.priority - b.priority);\r\n }\r\n\r\n /**\r\n * Get all connectors\r\n */\r\n public list(): Connector[] {\r\n return this.connectors;\r\n }\r\n\r\n /**\r\n * start all connectors\r\n */\r\n public async start(connectorsNames?: ConnectorName[]): Promise<void> {\r\n const connectorsList = connectorsNames\r\n ? this.connectors.filter((connector) => connectorsNames.includes(connector.name))\r\n : this.connectors;\r\n\r\n for (const connector of connectorsList) {\r\n await connector.boot();\r\n }\r\n\r\n for (const connector of connectorsList) {\r\n await connector.start();\r\n }\r\n }\r\n\r\n /**\r\n * Start all connectors in the given lifecycle phase.\r\n *\r\n * The production builder and dev preload split startup around app\r\n * code: early phase before app imports, late phase after. Within a\r\n * phase, all connectors `boot()` first, then all `start()`, so\r\n * cross-connector wiring inside the phase still works (e.g. socket\r\n * reads http's instance during its own boot).\r\n */\r\n public async startPhase(phase: ConnectorLifecyclePhase): Promise<void> {\r\n const phaseConnectors = this.connectors.filter(\r\n (connector) => connector.lifecyclePhase === phase,\r\n );\r\n\r\n for (const connector of phaseConnectors) {\r\n await connector.boot();\r\n }\r\n\r\n for (const connector of phaseConnectors) {\r\n await connector.start();\r\n }\r\n }\r\n\r\n /**\r\n * Start all connectors except the given ones\r\n */\r\n public async startWithout(excludedConnectors: ConnectorName[]): Promise<void> {\r\n await this.start(\r\n this.connectors\r\n .filter((connector) => !excludedConnectors.includes(connector.name))\r\n .map((connector) => connector.name),\r\n );\r\n }\r\n\r\n /**\r\n * Shutdown all connectors\r\n */\r\n public async shutdown(): Promise<void> {\r\n // App-owned teardown runs first, while every connector (db, cache, http) is\r\n // still up, so cleanup hooks can use them. Idempotent + error-isolated.\r\n await Application.runShutdownHooks();\r\n\r\n // Shut down connectors in reverse priority order. Copy the list first —\r\n // `reverse()` mutates in place, so reversing the live array would corrupt\r\n // the order on a second shutdown pass.\r\n for (const connector of [...this.connectors].reverse()) {\r\n try {\r\n await connector.shutdown();\r\n } catch (error) {\r\n try {\r\n // Awaited deliberately: the caller (`shutdownOnProcessKill`) calls\r\n // `process.exit(0)` as soon as this resolves, so an un-awaited log is\r\n // a log that never happens.\r\n await log.error(\"connectors\", \"shutdown\", error as Error, { connector: connector.name });\r\n } catch {\r\n // Reporting a failure must never become a worse failure than the one\r\n // being reported. `Logger.log()` fans out to channels without\r\n // isolating them, so a channel that throws synchronously rejects this\r\n // call — and this call sits INSIDE the catch, so that rejection\r\n // escapes `shutdown()` entirely: the remaining connectors are never\r\n // torn down and `process.exit(0)` never runs, leaving the process\r\n // alive on the handles they still hold.\r\n //\r\n // Swallowed rather than re-reported: the only channel we could report\r\n // it through is the one that just threw.\r\n }\r\n }\r\n }\r\n\r\n // `Logger.log()` hands each entry to `channel.log()` without awaiting it, so\r\n // awaiting the calls above is not enough on its own — a buffered or async\r\n // channel (file, Sentry) still loses the entry when the process exits.\r\n // Drained once here rather than per failure: shutdown is exactly when\r\n // buffered logs must reach their destination, and re-draining every channel\r\n // once per failed connector buys nothing.\r\n await log.flush();\r\n }\r\n\r\n /**\r\n * Shutdown connectors on process kill\r\n *\r\n * Registers signal handlers that run an idempotent graceful shutdown:\r\n * - SIGINT: Ctrl+C on Unix, also caught on Windows but unreliable in child processes\r\n * - SIGTERM: Termination signal (Unix primarily)\r\n * - SIGHUP: registered on Windows (`win32`) only, to catch Ctrl+C / console close\r\n *\r\n * Note: no `beforeExit` handler is registered — `beforeExit` cannot reliably\r\n * await async teardown (the process exits once the callback returns), so\r\n * shutdown is driven entirely by the signal handlers above.\r\n */\r\n public shutdownOnProcessKill(): void {\r\n let isShuttingDown = false;\r\n\r\n const gracefulShutdown = async (signal: string) => {\r\n if (isShuttingDown) return;\r\n isShuttingDown = true;\r\n\r\n console.log(`\\nExiting...`);\r\n await this.shutdown();\r\n process.exit(0);\r\n };\r\n\r\n // Unix signals\r\n process.on(\"SIGINT\", () => gracefulShutdown(\"SIGINT\"));\r\n process.on(\"SIGTERM\", () => gracefulShutdown(\"SIGTERM\"));\r\n\r\n // Windows-specific: handle when process is about to exit\r\n // This is more reliable on Windows for spawned child processes\r\n if (process.platform === \"win32\") {\r\n // Handle Ctrl+C on Windows specifically\r\n process.on(\"SIGHUP\", () => gracefulShutdown(\"SIGHUP\"));\r\n }\r\n }\r\n}\r\n\r\nexport const connectorsManager = new ConnectorsManager();\r\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,IAAa,oBAAb,MAA+B;;;;CAS7B,AAAO,cAAc;oBALsB,CAAC;EAM1C,KAAK,SAAS,IAAI,gBAAgB,CAAC;EACnC,KAAK,SAAS,IAAI,gBAAgB,CAAC;EACnC,KAAK,SAAS,IAAI,cAAc,CAAC;EACjC,KAAK,SAAS,IAAI,kBAAkB,CAAC;EACrC,KAAK,SAAS,IAAI,gBAAgB,CAAC;EACnC,KAAK,SAAS,IAAI,eAAe,CAAC;EAClC,KAAK,SAAS,IAAI,iBAAiB,CAAC;EACpC,KAAK,SAAS,IAAI,gBAAgB,CAAC;EACnC,KAAK,SAAS,IAAI,uBAAuB,CAAC;EAC1C,KAAK,SAAS,IAAI,gBAAgB,CAAC;EACnC,KAAK,SAAS,IAAI,YAAY,CAAC;CACjC;;;;CAKA,AAAO,SAAS,GAAG,YAA+B;EAChD,KAAK,WAAW,KAAK,GAAG,UAAU;EAElC,KAAK,WAAW,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;CACxD;;;;CAKA,AAAO,OAAoB;EACzB,OAAO,KAAK;CACd;;;;CAKA,MAAa,MAAM,iBAAkD;EACnE,MAAM,iBAAiB,kBACnB,KAAK,WAAW,QAAQ,cAAc,gBAAgB,SAAS,UAAU,IAAI,CAAC,IAC9E,KAAK;EAET,KAAK,MAAM,aAAa,gBACtB,MAAM,UAAU,KAAK;EAGvB,KAAK,MAAM,aAAa,gBACtB,MAAM,UAAU,MAAM;CAE1B;;;;;;;;;;CAWA,MAAa,WAAW,OAA+C;EACrE,MAAM,kBAAkB,KAAK,WAAW,QACrC,cAAc,UAAU,mBAAmB,KAC9C;EAEA,KAAK,MAAM,aAAa,iBACtB,MAAM,UAAU,KAAK;EAGvB,KAAK,MAAM,aAAa,iBACtB,MAAM,UAAU,MAAM;CAE1B;;;;CAKA,MAAa,aAAa,oBAAoD;EAC5E,MAAM,KAAK,MACT,KAAK,WACF,QAAQ,cAAc,CAAC,mBAAmB,SAAS,UAAU,IAAI,CAAC,CAAC,CACnE,KAAK,cAAc,UAAU,IAAI,CACtC;CACF;;;;CAKA,MAAa,WAA0B;EAGrC,MAAM,YAAY,iBAAiB;EAKnC,KAAK,MAAM,aAAa,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,QAAQ,GACnD,IAAI;GACF,MAAM,UAAU,SAAS;EAC3B,SAAS,OAAO;GACd,IAAI;IAIF,MAAM,IAAI,MAAM,cAAc,YAAY,OAAgB,EAAE,WAAW,UAAU,KAAK,CAAC;GACzF,QAAQ,CAWR;EACF;EASF,MAAM,IAAI,MAAM;CAClB;;;;;;;;;;;;;CAcA,AAAO,wBAA8B;EACnC,IAAI,iBAAiB;EAErB,MAAM,mBAAmB,OAAO,WAAmB;GACjD,IAAI,gBAAgB;GACpB,iBAAiB;GAEjB,QAAQ,IAAI,cAAc;GAC1B,MAAM,KAAK,SAAS;GACpB,QAAQ,KAAK,CAAC;EAChB;EAGA,QAAQ,GAAG,gBAAgB,iBAAiB,QAAQ,CAAC;EACrD,QAAQ,GAAG,iBAAiB,iBAAiB,SAAS,CAAC;EAIvD,IAAI,QAAQ,aAAa,SAEvB,QAAQ,GAAG,gBAAgB,iBAAiB,QAAQ,CAAC;CAEzD;AACF;AAEA,MAAa,oBAAoB,IAAI,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"connectors-manager.mjs","names":[],"sources":["../../../../../../../core/src/connectors/connectors-manager.ts"],"sourcesContent":["import { Application } from \"../application\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AccessConnector } from \"./access-connector\";\r\nimport { AiConnector } from \"./ai-connector\";\r\nimport { CacheConnector } from \"./cache-connector\";\r\nimport { DatabaseConnector } from \"./database-connector\";\r\nimport { HeraldConnector } from \"./herald-connector\";\r\nimport { HttpConnector } from \"./http-connector\";\r\nimport { LoggerConnector } from \"./logger-connector\";\r\nimport { MailerConnector } from \"./mail-connector\";\r\nimport { NotificationsConnector } from \"./notifications-connector\";\r\nimport { SocketConnector } from \"./socket-connector\";\r\nimport { StorageConnector } from \"./storage.connector\";\r\nimport { ConnectorLifecyclePhase } from \"./types\";\r\nimport type { Connector, ConnectorName } from \"./types\";\r\n\r\nexport class ConnectorsManager {\r\n /**\r\n * Connectors list\r\n */\r\n private readonly connectors: Connector[] = [];\r\n\r\n /**\r\n * The names the framework's own connectors hold.\r\n *\r\n * Filled by the constructor's own registration calls rather than declared\r\n * beside them, so there is exactly ONE place a built-in name exists: the\r\n * connector that owns it. A second list — in the builder, in a config\r\n * validator, in a test double — is a list that drifts the moment a built-in\r\n * is added or renamed, and it drifts silently, because nothing compares the\r\n * two. Both halves of a connector's life read this one through\r\n * {@link isBuiltInName}: the build refuses a `connectors` array that claims\r\n * a built-in name, and so does registration.\r\n */\r\n private readonly builtInNames = new Set<string>();\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor() {\r\n this.registerBuiltIn(new LoggerConnector());\r\n this.registerBuiltIn(new MailerConnector());\r\n this.registerBuiltIn(new HttpConnector());\r\n this.registerBuiltIn(new DatabaseConnector());\r\n this.registerBuiltIn(new HeraldConnector());\r\n this.registerBuiltIn(new CacheConnector());\r\n this.registerBuiltIn(new StorageConnector());\r\n this.registerBuiltIn(new SocketConnector());\r\n this.registerBuiltIn(new NotificationsConnector());\r\n this.registerBuiltIn(new AccessConnector());\r\n this.registerBuiltIn(new AiConnector());\r\n }\r\n\r\n /**\r\n * Register a framework-owned connector and reserve its name.\r\n *\r\n * Private on purpose: an app connector must not be able to reserve a name,\r\n * and the set is only correct if every entry in it came from a connector\r\n * this class constructed itself.\r\n */\r\n private registerBuiltIn(...connectors: Connector[]): void {\r\n for (const connector of connectors) {\r\n this.builtInNames.add(connector.name);\r\n }\r\n\r\n this.register(...connectors);\r\n }\r\n\r\n /**\r\n * Whether the given name belongs to a framework built-in connector.\r\n *\r\n * Reserved, not merely taken: a configured connector carrying one of these\r\n * names would be skipped by {@link has} at boot while the build had already\r\n * drained its contribution, so the artifact would carry a connector the\r\n * server never runs. Callers refuse the name instead of skipping it.\r\n */\r\n public isBuiltInName(name: string): boolean {\r\n return this.builtInNames.has(name);\r\n }\r\n\r\n /**\r\n * Register a connector\r\n */\r\n public register(...connectors: Connector[]): void {\r\n this.connectors.push(...connectors);\r\n // sort connectors by priority\r\n this.connectors.sort((a, b) => a.priority - b.priority);\r\n }\r\n\r\n /**\r\n * Get all connectors\r\n */\r\n public list(): Connector[] {\r\n return this.connectors;\r\n }\r\n\r\n /**\r\n * Whether a connector with the given name is already registered.\r\n *\r\n * Names are the identity of a connector here, which is what lets a caller\r\n * that may run more than once per process register idempotently instead of\r\n * stacking duplicates that would each boot and shut down.\r\n */\r\n public has(name: ConnectorName): boolean {\r\n return this.connectors.some((connector) => connector.name === name);\r\n }\r\n\r\n /**\r\n * start all connectors\r\n */\r\n public async start(connectorsNames?: ConnectorName[]): Promise<void> {\r\n const connectorsList = connectorsNames\r\n ? this.connectors.filter((connector) => connectorsNames.includes(connector.name))\r\n : this.connectors;\r\n\r\n for (const connector of connectorsList) {\r\n await connector.boot();\r\n }\r\n\r\n for (const connector of connectorsList) {\r\n await connector.start();\r\n }\r\n }\r\n\r\n /**\r\n * Start all connectors in the given lifecycle phase.\r\n *\r\n * The production builder and dev preload split startup around app\r\n * code: early phase before app imports, late phase after. Within a\r\n * phase, all connectors `boot()` first, then all `start()`, so\r\n * cross-connector wiring inside the phase still works (e.g. socket\r\n * reads http's instance during its own boot).\r\n */\r\n public async startPhase(phase: ConnectorLifecyclePhase): Promise<void> {\r\n const phaseConnectors = this.connectors.filter(\r\n (connector) => connector.lifecyclePhase === phase,\r\n );\r\n\r\n for (const connector of phaseConnectors) {\r\n await connector.boot();\r\n }\r\n\r\n for (const connector of phaseConnectors) {\r\n await connector.start();\r\n }\r\n }\r\n\r\n /**\r\n * Start all connectors except the given ones\r\n */\r\n public async startWithout(excludedConnectors: ConnectorName[]): Promise<void> {\r\n await this.start(\r\n this.connectors\r\n .filter((connector) => !excludedConnectors.includes(connector.name))\r\n .map((connector) => connector.name),\r\n );\r\n }\r\n\r\n /**\r\n * Shutdown all connectors\r\n */\r\n public async shutdown(): Promise<void> {\r\n // App-owned teardown runs first, while every connector (db, cache, http) is\r\n // still up, so cleanup hooks can use them. Idempotent + error-isolated.\r\n await Application.runShutdownHooks();\r\n\r\n // Shut down connectors in reverse priority order. Copy the list first —\r\n // `reverse()` mutates in place, so reversing the live array would corrupt\r\n // the order on a second shutdown pass.\r\n for (const connector of [...this.connectors].reverse()) {\r\n try {\r\n await connector.shutdown();\r\n } catch (error) {\r\n try {\r\n // Awaited deliberately: the caller (`shutdownOnProcessKill`) calls\r\n // `process.exit(0)` as soon as this resolves, so an un-awaited log is\r\n // a log that never happens.\r\n await log.error(\"connectors\", \"shutdown\", error as Error, { connector: connector.name });\r\n } catch {\r\n // Reporting a failure must never become a worse failure than the one\r\n // being reported. `Logger.log()` fans out to channels without\r\n // isolating them, so a channel that throws synchronously rejects this\r\n // call — and this call sits INSIDE the catch, so that rejection\r\n // escapes `shutdown()` entirely: the remaining connectors are never\r\n // torn down and `process.exit(0)` never runs, leaving the process\r\n // alive on the handles they still hold.\r\n //\r\n // Swallowed rather than re-reported: the only channel we could report\r\n // it through is the one that just threw.\r\n }\r\n }\r\n }\r\n\r\n // `Logger.log()` hands each entry to `channel.log()` without awaiting it, so\r\n // awaiting the calls above is not enough on its own — a buffered or async\r\n // channel (file, Sentry) still loses the entry when the process exits.\r\n // Drained once here rather than per failure: shutdown is exactly when\r\n // buffered logs must reach their destination, and re-draining every channel\r\n // once per failed connector buys nothing.\r\n await log.flush();\r\n }\r\n\r\n /**\r\n * Shutdown connectors on process kill\r\n *\r\n * Registers signal handlers that run an idempotent graceful shutdown:\r\n * - SIGINT: Ctrl+C on Unix, also caught on Windows but unreliable in child processes\r\n * - SIGTERM: Termination signal (Unix primarily)\r\n * - SIGHUP: registered on Windows (`win32`) only, to catch Ctrl+C / console close\r\n *\r\n * Note: no `beforeExit` handler is registered — `beforeExit` cannot reliably\r\n * await async teardown (the process exits once the callback returns), so\r\n * shutdown is driven entirely by the signal handlers above.\r\n */\r\n public shutdownOnProcessKill(): void {\r\n let isShuttingDown = false;\r\n\r\n const gracefulShutdown = async (signal: string) => {\r\n if (isShuttingDown) return;\r\n isShuttingDown = true;\r\n\r\n console.log(`\\nExiting...`);\r\n await this.shutdown();\r\n process.exit(0);\r\n };\r\n\r\n // Unix signals\r\n process.on(\"SIGINT\", () => gracefulShutdown(\"SIGINT\"));\r\n process.on(\"SIGTERM\", () => gracefulShutdown(\"SIGTERM\"));\r\n\r\n // Windows-specific: handle when process is about to exit\r\n // This is more reliable on Windows for spawned child processes\r\n if (process.platform === \"win32\") {\r\n // Handle Ctrl+C on Windows specifically\r\n process.on(\"SIGHUP\", () => gracefulShutdown(\"SIGHUP\"));\r\n }\r\n }\r\n}\r\n\r\nexport const connectorsManager = new ConnectorsManager();\r\n"],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region ../core/src/connectors/describe-server-address.ts
|
|
2
|
+
/**
|
|
3
|
+
* What to say once the HTTP server has bound.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is not one template string
|
|
6
|
+
*
|
|
7
|
+
* `http-connector.ts` used to bind `httpConfig.port` and then announce
|
|
8
|
+
* `config.get("app.baseUrl")` — a different value, read from a different
|
|
9
|
+
* environment variable (`PORT` vs `BASE_URL`), with nothing checking the two
|
|
10
|
+
* agreed. On 2026-08-24 a server listening on 3000 printed
|
|
11
|
+
* `Server ready at http://localhost:41900`. **Following the line the server
|
|
12
|
+
* itself printed gave connection refused, on a server that was working.** The
|
|
13
|
+
* reference app carried the rule as a comment in `.env` — "must agree with the
|
|
14
|
+
* port in BASE_URL above" — and a rule a comment enforces is not enforced.
|
|
15
|
+
*
|
|
16
|
+
* So the ready line now states the address actually bound. `app.baseUrl` is a
|
|
17
|
+
* separate claim, and it is reported separately.
|
|
18
|
+
*
|
|
19
|
+
* ## Why a disagreement is not fatal
|
|
20
|
+
*
|
|
21
|
+
* A reverse-proxied deployment legitimately binds `0.0.0.0:3000` and publishes
|
|
22
|
+
* `https://example.com`. Refusing to boot on a mismatch would break the correct
|
|
23
|
+
* case, and warning on it would fire on every correct production boot — a
|
|
24
|
+
* warning that always fires is one nobody reads.
|
|
25
|
+
*
|
|
26
|
+
* The mismatch worth a warning is the ACTIONABLE one: a base URL that points at
|
|
27
|
+
* **loopback on a port nothing is listening on**, because someone will paste it
|
|
28
|
+
* into a browser and conclude the server is broken. A non-loopback base URL is
|
|
29
|
+
* a deployment address, reported plainly and never flagged.
|
|
30
|
+
*/
|
|
31
|
+
/** Hosts that all mean "this machine" for the purpose of comparing addresses. */
|
|
32
|
+
const LOOPBACK_HOSTS = new Set([
|
|
33
|
+
"localhost",
|
|
34
|
+
"127.0.0.1",
|
|
35
|
+
"::1",
|
|
36
|
+
"[::1]",
|
|
37
|
+
"0.0.0.0",
|
|
38
|
+
""
|
|
39
|
+
]);
|
|
40
|
+
function parseUrl(value) {
|
|
41
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
42
|
+
try {
|
|
43
|
+
return new URL(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function isLoopback(url) {
|
|
49
|
+
return LOOPBACK_HOSTS.has(url.hostname);
|
|
50
|
+
}
|
|
51
|
+
/** `:3000` explicitly, or the protocol's default when the URL omits it. */
|
|
52
|
+
function effectivePort(url) {
|
|
53
|
+
if (url.port) return url.port;
|
|
54
|
+
return url.protocol === "https:" ? "443" : "80";
|
|
55
|
+
}
|
|
56
|
+
function describeServerAddress(boundAddress, baseUrl) {
|
|
57
|
+
const bound = parseUrl(boundAddress);
|
|
58
|
+
const configured = parseUrl(baseUrl);
|
|
59
|
+
if (!bound) return { ready: `Server ready at ${configured?.href ?? boundAddress ?? baseUrl}` };
|
|
60
|
+
const ready = `Server ready at ${boundAddress}`;
|
|
61
|
+
if (!configured) return { ready };
|
|
62
|
+
if (!isLoopback(configured)) return {
|
|
63
|
+
ready,
|
|
64
|
+
publicUrl: `Public URL (app.baseUrl): ${baseUrl}`
|
|
65
|
+
};
|
|
66
|
+
if (effectivePort(configured) === effectivePort(bound)) return { ready };
|
|
67
|
+
return {
|
|
68
|
+
ready,
|
|
69
|
+
warning: `app.baseUrl is ${baseUrl}, but the server bound port ${effectivePort(bound)}. Nothing is listening on port ${effectivePort(configured)} — check BASE_URL and PORT, including anything exported in your shell.`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
export { describeServerAddress };
|
|
75
|
+
//# sourceMappingURL=describe-server-address.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe-server-address.mjs","names":[],"sources":["../../../../../../../core/src/connectors/describe-server-address.ts"],"sourcesContent":["/**\n * What to say once the HTTP server has bound.\n *\n * ## Why this is not one template string\n *\n * `http-connector.ts` used to bind `httpConfig.port` and then announce\n * `config.get(\"app.baseUrl\")` — a different value, read from a different\n * environment variable (`PORT` vs `BASE_URL`), with nothing checking the two\n * agreed. On 2026-08-24 a server listening on 3000 printed\n * `Server ready at http://localhost:41900`. **Following the line the server\n * itself printed gave connection refused, on a server that was working.** The\n * reference app carried the rule as a comment in `.env` — \"must agree with the\n * port in BASE_URL above\" — and a rule a comment enforces is not enforced.\n *\n * So the ready line now states the address actually bound. `app.baseUrl` is a\n * separate claim, and it is reported separately.\n *\n * ## Why a disagreement is not fatal\n *\n * A reverse-proxied deployment legitimately binds `0.0.0.0:3000` and publishes\n * `https://example.com`. Refusing to boot on a mismatch would break the correct\n * case, and warning on it would fire on every correct production boot — a\n * warning that always fires is one nobody reads.\n *\n * The mismatch worth a warning is the ACTIONABLE one: a base URL that points at\n * **loopback on a port nothing is listening on**, because someone will paste it\n * into a browser and conclude the server is broken. A non-loopback base URL is\n * a deployment address, reported plainly and never flagged.\n */\n\n/** Hosts that all mean \"this machine\" for the purpose of comparing addresses. */\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\", \"0.0.0.0\", \"\"]);\n\nexport type ServerAddressReport = {\n /** Where the server is genuinely reachable. Always present. */\n ready: string;\n /** The configured public address, when it is a deployment URL rather than a local one. */\n publicUrl?: string;\n /** Set only when `app.baseUrl` points at loopback on a port nothing bound. */\n warning?: string;\n};\n\nfunction parseUrl(value: unknown): URL | undefined {\n if (typeof value !== \"string\" || value.length === 0) return undefined;\n\n try {\n return new URL(value);\n } catch {\n // An unparseable base URL is a separate problem, and it is NOT evidence of\n // a port mismatch — reporting one from it would be a guess.\n return undefined;\n }\n}\n\nfunction isLoopback(url: URL): boolean {\n return LOOPBACK_HOSTS.has(url.hostname);\n}\n\n/** `:3000` explicitly, or the protocol's default when the URL omits it. */\nfunction effectivePort(url: URL): string {\n if (url.port) return url.port;\n\n return url.protocol === \"https:\" ? \"443\" : \"80\";\n}\n\nexport function describeServerAddress(\n boundAddress: string | undefined,\n baseUrl: unknown,\n): ServerAddressReport {\n const bound = parseUrl(boundAddress);\n const configured = parseUrl(baseUrl);\n\n // No bound address to report means the caller could not obtain one; the\n // configured URL is then the only thing we know, and saying nothing would be\n // worse than saying the one thing we have.\n if (!bound) {\n return { ready: `Server ready at ${configured?.href ?? boundAddress ?? baseUrl}` };\n }\n\n const ready = `Server ready at ${boundAddress}`;\n\n if (!configured) return { ready };\n\n if (!isLoopback(configured)) {\n return { ready, publicUrl: `Public URL (app.baseUrl): ${baseUrl}` };\n }\n\n if (effectivePort(configured) === effectivePort(bound)) {\n return { ready };\n }\n\n return {\n ready,\n warning:\n `app.baseUrl is ${baseUrl}, but the server bound port ${effectivePort(bound)}. ` +\n `Nothing is listening on port ${effectivePort(configured)} — ` +\n `check BASE_URL and PORT, including anything exported in your shell.`,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAM,iBAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;CAAS;CAAW;AAAE,CAAC;AAWxF,SAAS,SAAS,OAAiC;CACjD,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG,OAAO;CAE5D,IAAI;EACF,OAAO,IAAI,IAAI,KAAK;CACtB,QAAQ;EAGN;CACF;AACF;AAEA,SAAS,WAAW,KAAmB;CACrC,OAAO,eAAe,IAAI,IAAI,QAAQ;AACxC;;AAGA,SAAS,cAAc,KAAkB;CACvC,IAAI,IAAI,MAAM,OAAO,IAAI;CAEzB,OAAO,IAAI,aAAa,WAAW,QAAQ;AAC7C;AAEA,SAAgB,sBACd,cACA,SACqB;CACrB,MAAM,QAAQ,SAAS,YAAY;CACnC,MAAM,aAAa,SAAS,OAAO;CAKnC,IAAI,CAAC,OACH,OAAO,EAAE,OAAO,mBAAmB,YAAY,QAAQ,gBAAgB,UAAU;CAGnF,MAAM,QAAQ,mBAAmB;CAEjC,IAAI,CAAC,YAAY,OAAO,EAAE,MAAM;CAEhC,IAAI,CAAC,WAAW,UAAU,GACxB,OAAO;EAAE;EAAO,WAAW,6BAA6B;CAAU;CAGpE,IAAI,cAAc,UAAU,MAAM,cAAc,KAAK,GACnD,OAAO,EAAE,MAAM;CAGjB,OAAO;EACL;EACA,SACE,kBAAkB,QAAQ,8BAA8B,cAAc,KAAK,EAAE,iCAC7C,cAAc,UAAU,EAAE;CAE9D;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"http-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"mappings":";;;;;;;AAsCA;;cAAa,aAAA,SAAsB,aAAA;EAAA,SACjB,IAAA;EAAA,SACA,QAAA,GAAQ,iBAAA,CAAA,IAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EAmBb;;;;;EAAA,mBAZE,YAAA;EAV2B;;;EAAA,UAepC,IAAA,GAAO,eAAA;EAbO;;;;;EAoBX,IAAA,IAAI,OAAA;EAPP;;;;;;;EAAA,QA0CF,oBAAA;EA2FgB;;;;;EA3DX,KAAA,IAAS,OAAA;EAgGqB;;;;;EArC9B,OAAA,IAAW,OAAA;;;;EASX,QAAA,IAAY,OAAA;;;;;;EA4BlB,aAAA,CAAc,YAAA;AAAA"}
|
|
@@ -8,6 +8,7 @@ import "../application/index.mjs";
|
|
|
8
8
|
import { health } from "../http/health.mjs";
|
|
9
9
|
import { ConnectorLifecyclePhase, ConnectorPriority } from "./types.mjs";
|
|
10
10
|
import { BaseConnector } from "./base-connector.mjs";
|
|
11
|
+
import { describeServerAddress } from "./describe-server-address.mjs";
|
|
11
12
|
import config from "@mongez/config";
|
|
12
13
|
import { log } from "@warlock.js/logger";
|
|
13
14
|
import { colors } from "@mongez/copper";
|
|
@@ -89,13 +90,15 @@ var HttpConnector = class extends BaseConnector {
|
|
|
89
90
|
health.addRoutesRegisteredCheck(() => router.routeCount());
|
|
90
91
|
log.info("http", "routes", `${router.routeCount()} route(s) registered`);
|
|
91
92
|
try {
|
|
92
|
-
await this.http.listen({
|
|
93
|
+
const boundAddress = await this.http.listen({
|
|
93
94
|
port: httpConfig.port,
|
|
94
95
|
host: httpConfig.host || "localhost"
|
|
95
96
|
});
|
|
96
97
|
Application.setServedPort(httpConfig.port);
|
|
97
|
-
const
|
|
98
|
-
log.success(`http`, "connection",
|
|
98
|
+
const address = describeServerAddress(boundAddress, config.get("app.baseUrl"));
|
|
99
|
+
log.success(`http`, "connection", address.ready);
|
|
100
|
+
if (address.publicUrl) log.info(`http`, "connection", address.publicUrl);
|
|
101
|
+
if (address.warning) log.warn(`http`, "connection", address.warning);
|
|
99
102
|
} catch (error) {
|
|
100
103
|
await log.fatal("http", "connection", error);
|
|
101
104
|
await log.flush();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { Application } from \"../application\";\r\nimport { health } from \"../http/health\";\r\nimport { registerHttpPlugins } from \"../http/plugins\";\r\nimport { closeServerWithTimeout, FastifyInstance, getHttpServer, startHttpServer } from \"../http/server\";\r\nimport { router } from \"../router/router\";\r\nimport { Environment } from \"../utils\";\r\nimport { setBaseUrl } from \"../utils/urls\";\r\nimport { container } from \"./../container\";\r\nimport { BaseConnector } from \"./base-connector\";\r\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\r\n\r\nfunction environmentColor(environment: Environment) {\r\n switch (environment) {\r\n case \"development\":\r\n return colors.magentaBright(environment);\r\n case \"test\":\r\n return colors.yellowBright(environment);\r\n case \"production\":\r\n return colors.greenBright(environment);\r\n default:\r\n return colors.white(environment);\r\n }\r\n}\r\n\r\n/**\r\n * Default time to wait for in-flight requests to drain on shutdown before the\r\n * HTTP server is force-closed. Override via `http.gracefulShutdown.timeout`.\r\n */\r\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\r\n\r\n/**\r\n * HTTP Connector\r\n * Manages HTTP server (Fastify) lifecycle\r\n */\r\nexport class HttpConnector extends BaseConnector {\r\n public readonly name = \"http\";\r\n public readonly priority = ConnectorPriority.HTTP;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\r\n\r\n /**\r\n * Files that trigger HTTP server restart\r\n * Note: routes.ts changes will be handled by HMR with wildcard routing\r\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\r\n */\r\n protected readonly watchedFiles = [\"src/config/http.ts\", \"src/config/http.tsx\"];\r\n\r\n /**\r\n * Fastify Server instance\r\n */\r\n protected http?: FastifyInstance;\r\n\r\n /**\r\n * Boot the connector — construction only (create Fastify, register\r\n * plugins, populate container). Route scanning is deferred to\r\n * `start()` so it reads the router after app code has registered.\r\n */\r\n public async boot() {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig) return;\r\n\r\n const port = httpConfig.port;\r\n log.info(\r\n `http`,\r\n \"connection\",\r\n `Starting http server on port ${port} in ${environmentColor(Application.environment)} mode`,\r\n );\r\n\r\n this.http = startHttpServer(httpConfig.serverOptions);\r\n\r\n container.set(\"http.server\", this.http);\r\n\r\n await registerHttpPlugins(this.http);\r\n\r\n const baseUrl = config.get(\"app.baseUrl\");\r\n\r\n // update base url\r\n setBaseUrl(baseUrl);\r\n\r\n // Liveness/readiness endpoints — registered on Fastify before start() scans\r\n // the app router, so they exist by the time the server listens.\r\n this.registerHealthRoutes();\r\n }\r\n\r\n /**\r\n * Register the built-in liveness (`/health`) and readiness (`/ready`)\r\n * endpoints directly on the Fastify instance — infra routes, kept off the app\r\n * router so they're immune to HMR and never collide with route scanning. Opt\r\n * out via `http.health.enabled = false`; rename via `http.health.path` /\r\n * `http.health.readinessPath`.\r\n */\r\n private registerHealthRoutes(): void {\r\n if (!this.http) {\r\n return;\r\n }\r\n\r\n const healthConfig = config.get(\"http.health\");\r\n\r\n if (healthConfig?.enabled === false) {\r\n return;\r\n }\r\n\r\n const livenessPath = healthConfig?.path ?? \"/health\";\r\n const readinessPath = healthConfig?.readinessPath ?? \"/ready\";\r\n\r\n this.http.get(livenessPath, async (_request: any, reply: any) => {\r\n const result = health.liveness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n\r\n this.http.get(readinessPath, async (_request: any, reply: any) => {\r\n const result = await health.readiness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n }\r\n\r\n /**\r\n * Initialize HTTP server — bind app-registered routes to Fastify\r\n * then listen. Scanning here (not in `boot`) lets HTTP boot before\r\n * app code without losing routes.\r\n */\r\n public async start(): Promise<void> {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig || !this.http) return;\r\n\r\n if (Application.runtimeStrategy === \"development\") {\r\n router.scanDevServer(this.http);\r\n } else {\r\n router.scan(this.http);\r\n }\r\n\r\n // Surface the route table as a readiness signal + a boot log, so an empty\r\n // or partial route surface (e.g. a route module that failed to register) is\r\n // detectable instead of silently 404ing. `addRoutesRegisteredCheck` is\r\n // keyed by name, so re-running on restart is idempotent.\r\n health.addRoutesRegisteredCheck(() => router.routeCount());\r\n log.info(\"http\", \"routes\", `${router.routeCount()} route(s) registered`);\r\n\r\n try {\r\n // We can use the url of the server\r\n await this.http.listen({\r\n port: httpConfig.port,\r\n host: httpConfig.host || \"localhost\",\r\n });\r\n\r\n Application.setServedPort(httpConfig.port);\r\n\r\n const baseUrl = config.get(\"app.baseUrl\");\r\n\r\n log.success(`http`, \"connection\", `Server ready at ${baseUrl}`);\r\n } catch (error) {\r\n // A failed listen()/port-bind at boot means the app can't serve — fatal.\r\n // `log.fatal` reaches Sentry/file; `await log.flush()` drains buffered and\r\n // async channels before the process exits.\r\n await log.fatal(\"http\", \"connection\", error);\r\n await log.flush();\r\n\r\n process.exit(1); // stop the process, exit with error\r\n }\r\n\r\n this.active = true;\r\n }\r\n\r\n /**\r\n * Restart — needs a fresh Fastify instance since `start()` now\r\n * re-runs `router.scan()`, and re-scanning the same Fastify would\r\n * register duplicate route handlers.\r\n */\r\n public async restart(): Promise<void> {\r\n await this.shutdown();\r\n await this.boot();\r\n await this.start();\r\n }\r\n\r\n /**\r\n * Shutdown HTTP server\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) {\r\n return;\r\n }\r\n\r\n const server = getHttpServer();\r\n\r\n if (server) {\r\n const timeout = config.get(\"http.gracefulShutdown.timeout\", DEFAULT_SHUTDOWN_TIMEOUT);\r\n const drained = await closeServerWithTimeout(server, timeout);\r\n\r\n if (!drained) {\r\n log.warn(\r\n \"http\",\r\n \"shutdown\",\r\n `In-flight requests did not drain within ${timeout}ms; closing anyway`,\r\n );\r\n }\r\n }\r\n\r\n this.active = false;\r\n }\r\n\r\n /**\r\n * Override shouldRestart to handle routes.ts specially\r\n * routes.ts changes should NOT restart the server (use HMR instead)\r\n * Now receives config file paths directly from layer-executor\r\n */\r\n public shouldRestart(changedFiles: string[]): boolean {\r\n // Only restart for config changes, not routes\r\n return changedFiles.some((file) => {\r\n const relativePath = file.replace(/\\\\/g, \"/\");\r\n return this.watchedFiles.includes(relativePath);\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http-connector.mjs","names":[],"sources":["../../../../../../../core/src/connectors/http-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport { colors } from \"@mongez/copper\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { Application } from \"../application\";\r\nimport { health } from \"../http/health\";\r\nimport { registerHttpPlugins } from \"../http/plugins\";\r\nimport { closeServerWithTimeout, FastifyInstance, getHttpServer, startHttpServer } from \"../http/server\";\r\nimport { router } from \"../router/router\";\r\nimport { Environment } from \"../utils\";\r\nimport { setBaseUrl } from \"../utils/urls\";\r\nimport { container } from \"./../container\";\r\nimport { BaseConnector } from \"./base-connector\";\r\nimport { describeServerAddress } from \"./describe-server-address\";\r\nimport { ConnectorLifecyclePhase, ConnectorPriority } from \"./types\";\r\n\r\nfunction environmentColor(environment: Environment) {\r\n switch (environment) {\r\n case \"development\":\r\n return colors.magentaBright(environment);\r\n case \"test\":\r\n return colors.yellowBright(environment);\r\n case \"production\":\r\n return colors.greenBright(environment);\r\n default:\r\n return colors.white(environment);\r\n }\r\n}\r\n\r\n/**\r\n * Default time to wait for in-flight requests to drain on shutdown before the\r\n * HTTP server is force-closed. Override via `http.gracefulShutdown.timeout`.\r\n */\r\nconst DEFAULT_SHUTDOWN_TIMEOUT = 10_000;\r\n\r\n/**\r\n * HTTP Connector\r\n * Manages HTTP server (Fastify) lifecycle\r\n */\r\nexport class HttpConnector extends BaseConnector {\r\n public readonly name = \"http\";\r\n public readonly priority = ConnectorPriority.HTTP;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\r\n\r\n /**\r\n * Files that trigger HTTP server restart\r\n * Note: routes.ts changes will be handled by HMR with wildcard routing\r\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\r\n */\r\n protected readonly watchedFiles = [\"src/config/http.ts\", \"src/config/http.tsx\"];\r\n\r\n /**\r\n * Fastify Server instance\r\n */\r\n protected http?: FastifyInstance;\r\n\r\n /**\r\n * Boot the connector — construction only (create Fastify, register\r\n * plugins, populate container). Route scanning is deferred to\r\n * `start()` so it reads the router after app code has registered.\r\n */\r\n public async boot() {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig) return;\r\n\r\n const port = httpConfig.port;\r\n log.info(\r\n `http`,\r\n \"connection\",\r\n `Starting http server on port ${port} in ${environmentColor(Application.environment)} mode`,\r\n );\r\n\r\n this.http = startHttpServer(httpConfig.serverOptions);\r\n\r\n container.set(\"http.server\", this.http);\r\n\r\n await registerHttpPlugins(this.http);\r\n\r\n const baseUrl = config.get(\"app.baseUrl\");\r\n\r\n // update base url\r\n setBaseUrl(baseUrl);\r\n\r\n // Liveness/readiness endpoints — registered on Fastify before start() scans\r\n // the app router, so they exist by the time the server listens.\r\n this.registerHealthRoutes();\r\n }\r\n\r\n /**\r\n * Register the built-in liveness (`/health`) and readiness (`/ready`)\r\n * endpoints directly on the Fastify instance — infra routes, kept off the app\r\n * router so they're immune to HMR and never collide with route scanning. Opt\r\n * out via `http.health.enabled = false`; rename via `http.health.path` /\r\n * `http.health.readinessPath`.\r\n */\r\n private registerHealthRoutes(): void {\r\n if (!this.http) {\r\n return;\r\n }\r\n\r\n const healthConfig = config.get(\"http.health\");\r\n\r\n if (healthConfig?.enabled === false) {\r\n return;\r\n }\r\n\r\n const livenessPath = healthConfig?.path ?? \"/health\";\r\n const readinessPath = healthConfig?.readinessPath ?? \"/ready\";\r\n\r\n this.http.get(livenessPath, async (_request: any, reply: any) => {\r\n const result = health.liveness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n\r\n this.http.get(readinessPath, async (_request: any, reply: any) => {\r\n const result = await health.readiness();\r\n\r\n return reply.code(result.status === \"ok\" ? 200 : 503).send(result);\r\n });\r\n }\r\n\r\n /**\r\n * Initialize HTTP server — bind app-registered routes to Fastify\r\n * then listen. Scanning here (not in `boot`) lets HTTP boot before\r\n * app code without losing routes.\r\n */\r\n public async start(): Promise<void> {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig || !this.http) return;\r\n\r\n if (Application.runtimeStrategy === \"development\") {\r\n router.scanDevServer(this.http);\r\n } else {\r\n router.scan(this.http);\r\n }\r\n\r\n // Surface the route table as a readiness signal + a boot log, so an empty\r\n // or partial route surface (e.g. a route module that failed to register) is\r\n // detectable instead of silently 404ing. `addRoutesRegisteredCheck` is\r\n // keyed by name, so re-running on restart is idempotent.\r\n health.addRoutesRegisteredCheck(() => router.routeCount());\r\n log.info(\"http\", \"routes\", `${router.routeCount()} route(s) registered`);\r\n\r\n try {\r\n // `listen()` RESOLVES with the address it actually bound — which is the\r\n // only address worth announcing. See `describe-server-address.ts` for why\r\n // announcing `app.baseUrl` here instead was a defect rather than a\r\n // shortcut.\r\n const boundAddress = await this.http.listen({\r\n port: httpConfig.port,\r\n host: httpConfig.host || \"localhost\",\r\n });\r\n\r\n Application.setServedPort(httpConfig.port);\r\n\r\n const address = describeServerAddress(boundAddress, config.get(\"app.baseUrl\"));\r\n\r\n log.success(`http`, \"connection\", address.ready);\r\n\r\n if (address.publicUrl) {\r\n log.info(`http`, \"connection\", address.publicUrl);\r\n }\r\n\r\n if (address.warning) {\r\n log.warn(`http`, \"connection\", address.warning);\r\n }\r\n } catch (error) {\r\n // A failed listen()/port-bind at boot means the app can't serve — fatal.\r\n // `log.fatal` reaches Sentry/file; `await log.flush()` drains buffered and\r\n // async channels before the process exits.\r\n await log.fatal(\"http\", \"connection\", error);\r\n await log.flush();\r\n\r\n process.exit(1); // stop the process, exit with error\r\n }\r\n\r\n this.active = true;\r\n }\r\n\r\n /**\r\n * Restart — needs a fresh Fastify instance since `start()` now\r\n * re-runs `router.scan()`, and re-scanning the same Fastify would\r\n * register duplicate route handlers.\r\n */\r\n public async restart(): Promise<void> {\r\n await this.shutdown();\r\n await this.boot();\r\n await this.start();\r\n }\r\n\r\n /**\r\n * Shutdown HTTP server\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) {\r\n return;\r\n }\r\n\r\n const server = getHttpServer();\r\n\r\n if (server) {\r\n const timeout = config.get(\"http.gracefulShutdown.timeout\", DEFAULT_SHUTDOWN_TIMEOUT);\r\n const drained = await closeServerWithTimeout(server, timeout);\r\n\r\n if (!drained) {\r\n log.warn(\r\n \"http\",\r\n \"shutdown\",\r\n `In-flight requests did not drain within ${timeout}ms; closing anyway`,\r\n );\r\n }\r\n }\r\n\r\n this.active = false;\r\n }\r\n\r\n /**\r\n * Override shouldRestart to handle routes.ts specially\r\n * routes.ts changes should NOT restart the server (use HMR instead)\r\n * Now receives config file paths directly from layer-executor\r\n */\r\n public shouldRestart(changedFiles: string[]): boolean {\r\n // Only restart for config changes, not routes\r\n return changedFiles.some((file) => {\r\n const relativePath = file.replace(/\\\\/g, \"/\");\r\n return this.watchedFiles.includes(relativePath);\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { Connector, ConnectorLifecyclePhase, ConnectorName, ConnectorPriority } from "./types.mjs";
|
|
1
|
+
import { Connector, ConnectorBuildContext, ConnectorBuildContribution, ConnectorBuildGenerateResult, ConnectorEsbuildPatch, ConnectorLifecyclePhase, ConnectorName, ConnectorPriority } from "./types.mjs";
|
|
2
2
|
import { BaseConnector } from "./base-connector.mjs";
|
|
3
3
|
import { AccessConnector } from "./access-connector.mjs";
|
|
4
4
|
import { AiConnector } from "./ai-connector.mjs";
|
|
5
|
+
import { assertNoReservedConnectorNames } from "./assert-no-reserved-connector-names.mjs";
|
|
6
|
+
import { assertUniqueConnectorNames } from "./assert-unique-connector-names.mjs";
|
|
5
7
|
import { CacheConnector } from "./cache-connector.mjs";
|
|
6
8
|
import { HeraldConnector } from "./herald-connector.mjs";
|
|
7
9
|
import { ConnectorsManager, connectorsManager } from "./connectors-manager.mjs";
|
|
@@ -10,5 +12,6 @@ import { HttpConnector } from "./http-connector.mjs";
|
|
|
10
12
|
import { LoggerConnector } from "./logger-connector.mjs";
|
|
11
13
|
import { MailerConnector } from "./mail-connector.mjs";
|
|
12
14
|
import { NotificationsConnector } from "./notifications-connector.mjs";
|
|
15
|
+
import { RegisterConfiguredConnectorsOptions, registerConfiguredConnectors } from "./register-configured-connectors.mjs";
|
|
13
16
|
import { SocketConnector } from "./socket-connector.mjs";
|
|
14
17
|
import { StorageConnector } from "./storage.connector.mjs";
|
package/esm/connectors/index.mjs
CHANGED
|
@@ -3,8 +3,8 @@ import { BaseConnector } from "./base-connector.mjs";
|
|
|
3
3
|
import { AccessConnector } from "./access-connector.mjs";
|
|
4
4
|
import { AiConnector } from "./ai-connector.mjs";
|
|
5
5
|
import { CacheConnector } from "./cache-connector.mjs";
|
|
6
|
-
import { HeraldConnector } from "./herald-connector.mjs";
|
|
7
6
|
import { DatabaseConnector } from "./database-connector.mjs";
|
|
7
|
+
import { HeraldConnector } from "./herald-connector.mjs";
|
|
8
8
|
import { HttpConnector } from "./http-connector.mjs";
|
|
9
9
|
import { LoggerConnector } from "./logger-connector.mjs";
|
|
10
10
|
import { MailerConnector } from "./mail-connector.mjs";
|
|
@@ -12,5 +12,8 @@ import { NotificationsConnector } from "./notifications-connector.mjs";
|
|
|
12
12
|
import { SocketConnector } from "./socket-connector.mjs";
|
|
13
13
|
import { StorageConnector } from "./storage.connector.mjs";
|
|
14
14
|
import { ConnectorsManager, connectorsManager } from "./connectors-manager.mjs";
|
|
15
|
+
import { assertNoReservedConnectorNames } from "./assert-no-reserved-connector-names.mjs";
|
|
16
|
+
import { assertUniqueConnectorNames } from "./assert-unique-connector-names.mjs";
|
|
17
|
+
import { registerConfiguredConnectors } from "./register-configured-connectors.mjs";
|
|
15
18
|
|
|
16
19
|
export { };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Connector } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/connectors/register-configured-connectors.d.ts
|
|
4
|
+
type RegisterConfiguredConnectorsOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* The connector names, in order, that the build resolved. The generated
|
|
7
|
+
* production entry bakes this in; every other caller omits it.
|
|
8
|
+
*/
|
|
9
|
+
expectedNames?: string[];
|
|
10
|
+
};
|
|
11
|
+
declare function registerConfiguredConnectors(connectors?: Connector[], options?: RegisterConfiguredConnectorsOptions): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { RegisterConfiguredConnectorsOptions, registerConfiguredConnectors };
|
|
14
|
+
//# sourceMappingURL=register-configured-connectors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-configured-connectors.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/register-configured-connectors.ts"],"mappings":";;;KAwCY,mCAAA;;AAAZ;;;EAKE,aAAa;AAAA;AAAA,iBAGC,4BAAA,CACd,UAAA,GAAa,SAAA,IACb,OAAA,GAAS,mCAAwC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { connectorsManager } from "./connectors-manager.mjs";
|
|
2
|
+
import { assertNoReservedConnectorNames } from "./assert-no-reserved-connector-names.mjs";
|
|
3
|
+
import { assertUniqueConnectorNames } from "./assert-unique-connector-names.mjs";
|
|
4
|
+
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../core/src/connectors/register-configured-connectors.ts
|
|
7
|
+
/**
|
|
8
|
+
* Registration half of the `connectors` config key.
|
|
9
|
+
*
|
|
10
|
+
* ONE key drives both halves of a connector's life: `warlock build` reads the
|
|
11
|
+
* same array to drain each connector's build contribution, and this function
|
|
12
|
+
* registers that array with the connectors manager so the runtime boots it.
|
|
13
|
+
* Reading them from one place is the whole point — a bundle "built for" web
|
|
14
|
+
* that "boots without" web is a green build with a missing client bundle, and
|
|
15
|
+
* the failure is silent. A second key would let the two drift; this cannot.
|
|
16
|
+
*
|
|
17
|
+
* Two callers, two ways in:
|
|
18
|
+
*
|
|
19
|
+
* - The generated production entry PASSES the array explicitly. Inside a
|
|
20
|
+
* bundled artifact nothing ever loads `warlock.config.ts` — the build
|
|
21
|
+
* imports it statically so esbuild bakes it into the bundle, then hands the
|
|
22
|
+
* array straight here. No config manager, no runtime file read. It also
|
|
23
|
+
* passes `expectedNames` — the list the build resolved — so a config that
|
|
24
|
+
* resolves differently at startup is refused instead of booted.
|
|
25
|
+
* - The dev preloader OMITS the array and lets this read the already-resolved
|
|
26
|
+
* warlock config, which dev has loaded by then. It also omits
|
|
27
|
+
* `expectedNames`, so dev gets NO drift check — deliberately. Drift is a
|
|
28
|
+
* relationship between what a build baked and what a runtime registers, and
|
|
29
|
+
* dev has no build: there is nothing to drift from, and inventing a second
|
|
30
|
+
* source of truth to compare against would create the disagreement this
|
|
31
|
+
* check exists to prevent. The absence of a check on this path is the
|
|
32
|
+
* design, not an omission — do not harmonise it with the production path.
|
|
33
|
+
* A conditional config that works in dev and is refused at production boot
|
|
34
|
+
* is this working as intended, not a gap.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent either way: a connector whose name an EARLIER CALL in the same
|
|
37
|
+
* process already registered is skipped rather than stacked, since a duplicate
|
|
38
|
+
* would boot and shut down twice. A built-in's name is not that case — it is
|
|
39
|
+
* reserved, and an array claiming one is refused rather than skipped.
|
|
40
|
+
*/
|
|
41
|
+
function registerConfiguredConnectors(connectors, options = {}) {
|
|
42
|
+
const registering = connectors ?? configuredConnectors();
|
|
43
|
+
assertUniqueConnectorNames(registering);
|
|
44
|
+
assertNoReservedConnectorNames(registering);
|
|
45
|
+
if (options.expectedNames) assertNoConnectorDrift(options.expectedNames, registering);
|
|
46
|
+
for (const connector of registering) {
|
|
47
|
+
if (connectorsManager.has(connector.name)) continue;
|
|
48
|
+
connectorsManager.register(connector);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Refuses a boot whose connector list is not the one the build was made from.
|
|
53
|
+
*
|
|
54
|
+
* `connectors` in warlock.config is a normal expression, so it may read the
|
|
55
|
+
* environment — the build resolves it once, and the artifact re-evaluates the
|
|
56
|
+
* same source at startup. Nothing forces the two results to agree, and when
|
|
57
|
+
* they do not the failure is silent: the bundle carries what a connector
|
|
58
|
+
* contributed at build time while the runtime never boots it, or boots one the
|
|
59
|
+
* build never prepared for.
|
|
60
|
+
*
|
|
61
|
+
* Order counts as a difference. The build drains connector contributions
|
|
62
|
+
* SEQUENTIALLY in config-array order — each one writing files and merging
|
|
63
|
+
* esbuild options on top of what the previous ones left — so the same names in
|
|
64
|
+
* another order describe an artifact produced by a drain sequence this build
|
|
65
|
+
* never ran. Start order is NOT the reason: connectors start in PRIORITY order,
|
|
66
|
+
* and the array's order only breaks ties between equal priorities.
|
|
67
|
+
*
|
|
68
|
+
* Names only — this compares WHICH connectors are present, never how any of
|
|
69
|
+
* them is configured, so it cannot catch a connector pointed at the wrong
|
|
70
|
+
* database. Saying so in the message is the point: a check that reads as
|
|
71
|
+
* broader than it is buys false confidence.
|
|
72
|
+
*/
|
|
73
|
+
function assertNoConnectorDrift(expectedNames, connectors) {
|
|
74
|
+
const actualNames = connectors.map((connector) => connector.name);
|
|
75
|
+
if (expectedNames.length === actualNames.length && expectedNames.every((name, index) => name === actualNames[index])) return;
|
|
76
|
+
throw new Error(`The connector list this server booted with does not match the list its build was made from.
|
|
77
|
+
expected (resolved when the app was built): ${formatNames(expectedNames)}\n actual (handed over at boot): ${formatNames(actualNames)}\nThis compares the set and order of connectors only, and says nothing about how any connector is configured. It usually means \`connectors\` in warlock.config resolves differently here than it did at build time — rebuild the app in the same configuration environment this server runs in, or make the array resolve to the same connectors in both.`);
|
|
78
|
+
}
|
|
79
|
+
function formatNames(names) {
|
|
80
|
+
return `[${names.map((name) => JSON.stringify(name)).join(", ")}]`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The connectors the resolved warlock config declares.
|
|
84
|
+
*
|
|
85
|
+
* Reached only when the caller omitted the array. An unloaded config here is
|
|
86
|
+
* not "nothing to register" — it is a caller asking this function to read a
|
|
87
|
+
* source that does not exist yet, so it throws instead of returning empty.
|
|
88
|
+
* Returning empty is how the production entry once registered NOTHING while
|
|
89
|
+
* reporting success: the bundle booted none of the connectors it was built
|
|
90
|
+
* for, and no line of output said so.
|
|
91
|
+
*/
|
|
92
|
+
function configuredConnectors() {
|
|
93
|
+
if (!warlockConfigManager.isLoaded) throw new Error("registerConfiguredConnectors: warlock config is not loaded; pass the connectors array explicitly or load the config first");
|
|
94
|
+
return warlockConfigManager.get("connectors") ?? [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
export { registerConfiguredConnectors };
|
|
99
|
+
//# sourceMappingURL=register-configured-connectors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-configured-connectors.mjs","names":[],"sources":["../../../../../../../core/src/connectors/register-configured-connectors.ts"],"sourcesContent":["/**\n * Registration half of the `connectors` config key.\n *\n * ONE key drives both halves of a connector's life: `warlock build` reads the\n * same array to drain each connector's build contribution, and this function\n * registers that array with the connectors manager so the runtime boots it.\n * Reading them from one place is the whole point — a bundle \"built for\" web\n * that \"boots without\" web is a green build with a missing client bundle, and\n * the failure is silent. A second key would let the two drift; this cannot.\n *\n * Two callers, two ways in:\n *\n * - The generated production entry PASSES the array explicitly. Inside a\n * bundled artifact nothing ever loads `warlock.config.ts` — the build\n * imports it statically so esbuild bakes it into the bundle, then hands the\n * array straight here. No config manager, no runtime file read. It also\n * passes `expectedNames` — the list the build resolved — so a config that\n * resolves differently at startup is refused instead of booted.\n * - The dev preloader OMITS the array and lets this read the already-resolved\n * warlock config, which dev has loaded by then. It also omits\n * `expectedNames`, so dev gets NO drift check — deliberately. Drift is a\n * relationship between what a build baked and what a runtime registers, and\n * dev has no build: there is nothing to drift from, and inventing a second\n * source of truth to compare against would create the disagreement this\n * check exists to prevent. The absence of a check on this path is the\n * design, not an omission — do not harmonise it with the production path.\n * A conditional config that works in dev and is refused at production boot\n * is this working as intended, not a gap.\n *\n * Idempotent either way: a connector whose name an EARLIER CALL in the same\n * process already registered is skipped rather than stacked, since a duplicate\n * would boot and shut down twice. A built-in's name is not that case — it is\n * reserved, and an array claiming one is refused rather than skipped.\n */\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { assertNoReservedConnectorNames } from \"./assert-no-reserved-connector-names\";\nimport { assertUniqueConnectorNames } from \"./assert-unique-connector-names\";\nimport { connectorsManager } from \"./connectors-manager\";\nimport type { Connector } from \"./types\";\n\nexport type RegisterConfiguredConnectorsOptions = {\n /**\n * The connector names, in order, that the build resolved. The generated\n * production entry bakes this in; every other caller omits it.\n */\n expectedNames?: string[];\n};\n\nexport function registerConfiguredConnectors(\n connectors?: Connector[],\n options: RegisterConfiguredConnectorsOptions = {},\n): void {\n const registering = connectors ?? configuredConnectors();\n\n // First, and before the drift check: a name listed twice is registered once\n // here but was drained twice by the build, and the two lists the drift check\n // compares are identical in that case — so it would wave the array through\n // and the asymmetry would survive into the boot. Reported as the duplicate it\n // is, rather than as whatever the next check happens to make of it.\n assertUniqueConnectorNames(registering);\n\n // Then the names that were taken before this array was ever read. The skip\n // below treats an already-held name as idempotency, which is right for a name\n // an EARLIER CALL registered — the same array arriving twice in one process —\n // and wrong for a built-in: nothing the app configured put that name there, so\n // the skip would drop a connector the app asked for and the build already\n // prepared artifacts for. Same call the build makes, so neither half can be\n // reached with an array the other refuses.\n assertNoReservedConnectorNames(registering);\n\n // Before anything is registered: a half-registered manager is worse than a\n // refused boot, because the phases would start on top of it.\n if (options.expectedNames) {\n assertNoConnectorDrift(options.expectedNames, registering);\n }\n\n for (const connector of registering) {\n if (connectorsManager.has(connector.name)) continue;\n\n connectorsManager.register(connector);\n }\n}\n\n/**\n * Refuses a boot whose connector list is not the one the build was made from.\n *\n * `connectors` in warlock.config is a normal expression, so it may read the\n * environment — the build resolves it once, and the artifact re-evaluates the\n * same source at startup. Nothing forces the two results to agree, and when\n * they do not the failure is silent: the bundle carries what a connector\n * contributed at build time while the runtime never boots it, or boots one the\n * build never prepared for.\n *\n * Order counts as a difference. The build drains connector contributions\n * SEQUENTIALLY in config-array order — each one writing files and merging\n * esbuild options on top of what the previous ones left — so the same names in\n * another order describe an artifact produced by a drain sequence this build\n * never ran. Start order is NOT the reason: connectors start in PRIORITY order,\n * and the array's order only breaks ties between equal priorities.\n *\n * Names only — this compares WHICH connectors are present, never how any of\n * them is configured, so it cannot catch a connector pointed at the wrong\n * database. Saying so in the message is the point: a check that reads as\n * broader than it is buys false confidence.\n */\nfunction assertNoConnectorDrift(expectedNames: string[], connectors: Connector[]): void {\n const actualNames = connectors.map((connector) => connector.name);\n\n const matches =\n expectedNames.length === actualNames.length &&\n expectedNames.every((name, index) => name === actualNames[index]);\n\n if (matches) return;\n\n throw new Error(\n \"The connector list this server booted with does not match the list its build was made from.\\n\" +\n ` expected (resolved when the app was built): ${formatNames(expectedNames)}\\n` +\n ` actual (handed over at boot): ${formatNames(actualNames)}\\n` +\n \"This compares the set and order of connectors only, and says nothing about how any connector is configured. \" +\n \"It usually means `connectors` in warlock.config resolves differently here than it did at build time — \" +\n \"rebuild the app in the same configuration environment this server runs in, \" +\n \"or make the array resolve to the same connectors in both.\",\n );\n}\n\nfunction formatNames(names: string[]): string {\n return `[${names.map((name) => JSON.stringify(name)).join(\", \")}]`;\n}\n\n/**\n * The connectors the resolved warlock config declares.\n *\n * Reached only when the caller omitted the array. An unloaded config here is\n * not \"nothing to register\" — it is a caller asking this function to read a\n * source that does not exist yet, so it throws instead of returning empty.\n * Returning empty is how the production entry once registered NOTHING while\n * reporting success: the bundle booted none of the connectors it was built\n * for, and no line of output said so.\n */\nfunction configuredConnectors(): Connector[] {\n if (!warlockConfigManager.isLoaded) {\n throw new Error(\n \"registerConfiguredConnectors: warlock config is not loaded; pass the connectors array explicitly or load the config first\",\n );\n }\n\n return warlockConfigManager.get(\"connectors\") ?? [];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,6BACd,YACA,UAA+C,CAAC,GAC1C;CACN,MAAM,cAAc,cAAc,qBAAqB;CAOvD,2BAA2B,WAAW;CAStC,+BAA+B,WAAW;CAI1C,IAAI,QAAQ,eACV,uBAAuB,QAAQ,eAAe,WAAW;CAG3D,KAAK,MAAM,aAAa,aAAa;EACnC,IAAI,kBAAkB,IAAI,UAAU,IAAI,GAAG;EAE3C,kBAAkB,SAAS,SAAS;CACtC;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAS,uBAAuB,eAAyB,YAA+B;CACtF,MAAM,cAAc,WAAW,KAAK,cAAc,UAAU,IAAI;CAMhE,IAHE,cAAc,WAAW,YAAY,UACrC,cAAc,OAAO,MAAM,UAAU,SAAS,YAAY,MAAM,GAErD;CAEb,MAAM,IAAI,MACR;gDACmD,YAAY,aAAa,EAAE,oCACzC,YAAY,WAAW,EAAE,2VAKhE;AACF;AAEA,SAAS,YAAY,OAAyB;CAC5C,OAAO,IAAI,MAAM,KAAK,SAAS,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE;AAClE;;;;;;;;;;;AAYA,SAAS,uBAAoC;CAC3C,IAAI,CAAC,qBAAqB,UACxB,MAAM,IAAI,MACR,2HACF;CAGF,OAAO,qBAAqB,IAAI,YAAY,KAAK,CAAC;AACpD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"mappings":";;;;;;;AA8BA;;cAAa,eAAA,SAAwB,aAAA;EAAA,SACnB,IAAA,EAAM,aAAA;EAAA,SACN,QAAA,GAAQ,iBAAA,CAAA,MAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EASX;;;;;EAAA,mBAFA,YAAA;EAAA,UAET,MAAA,GAAS,MAAA;EAZgB;;;;;;;;EAAA,UAsBzB,aAAA;EAZS;;;EAiBN,IAAA,IAAI,OAAA;EAAJ;;;
|
|
1
|
+
{"version":3,"file":"socket-connector.d.mts","names":[],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"mappings":";;;;;;;AA8BA;;cAAa,eAAA,SAAwB,aAAA;EAAA,SACnB,IAAA,EAAM,aAAA;EAAA,SACN,QAAA,GAAQ,iBAAA,CAAA,MAAA;EAAA,SACR,cAAA,GAAc,uBAAA,CAAA,IAAA;EASX;;;;;EAAA,mBAFA,YAAA;EAAA,UAET,MAAA,GAAS,MAAA;EAZgB;;;;;;;;EAAA,UAsBzB,aAAA;EAZS;;;EAiBN,IAAA,IAAI,OAAA;EAAJ;;;EAuDA,KAAA,IAAS,OAAA;EAqBT;;;AAAmB;;;;;;EAAnB,QAAA,IAAY,OAAA;AAAA"}
|
|
@@ -59,7 +59,10 @@ var SocketConnector = class extends BaseConnector {
|
|
|
59
59
|
this.ownsRawServer = true;
|
|
60
60
|
}
|
|
61
61
|
container.set("socket.rawServer", server);
|
|
62
|
-
this.socket = new SocketServer(server,
|
|
62
|
+
this.socket = new SocketServer(server, {
|
|
63
|
+
destroyUpgrade: false,
|
|
64
|
+
...socketConfig.options
|
|
65
|
+
});
|
|
63
66
|
container.set("socket", this.socket);
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket-connector.mjs","names":["createHttpsServer","createHttpServer"],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { createServer as createHttpServer } from \"http\";\nimport { createServer as createHttpsServer } from \"https\";\nimport type { Server } from \"socket.io\";\nimport { container } from \"../container\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorName, ConnectorPriority } from \"./types\";\n\n/**\n * Shown when a project enables sockets (a `socket` config is present) but the\n * optional `socket.io` peer is not installed.\n */\nconst SOCKET_INSTALL_INSTRUCTIONS = `\nRealtime socket server requires the socket.io package.\nInstall it with:\n\n warlock add socket\n\nOr manually:\n\n npm install socket.io\n pnpm add socket.io\n yarn add socket.io\n`.trim();\n\n/**\n * Socket Connector\n * Manages Socket server (Socket.IO) lifecycle\n */\nexport class SocketConnector extends BaseConnector {\n public readonly name: ConnectorName = \"socket\";\n public readonly priority = ConnectorPriority.SOCKET;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Files that trigger Socket server restart\n * Note: routes.ts changes will be handled by HMR with wildcard routing\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\n */\n protected readonly watchedFiles = [\"src/config/socket.ts\"];\n\n protected socket?: Server;\n\n /**\n * Whether this connector created the underlying node HTTP(S) server.\n *\n * When `http` is present, socket binds onto the HTTP connector's shared\n * `fastify.server` and must NOT close it on shutdown — the HTTP connector\n * owns the graceful drain. Socket only closes the node server when it\n * created one itself (http absent). See {@link shutdown}.\n */\n protected ownsRawServer = false;\n\n /**\n * Boot the connector\n */\n public async boot() {\n const socketConfig = config.get(\"socket\");\n\n if (!socketConfig) return;\n\n // socket.io is an optional peer — load it lazily so projects that don't use\n // realtime sockets never need it installed (mirrors the mail/storage drivers).\n let SocketServer: typeof import(\"socket.io\").Server;\n try {\n ({ Server: SocketServer } = await import(\"socket.io\"));\n } catch {\n throw new Error(SOCKET_INSTALL_INSTRUCTIONS);\n }\n\n log.info(\"socket\", \"connection\", \"Starting Socket.IO server\");\n\n // now we have two cases\n // 1. http is used, then use it (shared — the HTTP connector owns it)\n // 2. http is not used, then create a new server (we own it)\n let server;\n if (container.has(\"http.server\")) {\n const fastify = container.get(\"http.server\");\n server = fastify.server;\n this.ownsRawServer = false;\n } else {\n server = socketConfig.ssl ? createHttpsServer() : createHttpServer();\n server.listen(socketConfig.port);\n this.ownsRawServer = true;\n }\n\n container.set(\"socket.rawServer\", server);\n\n this.socket = new SocketServer(server, socketConfig.options);\n\n container.set(\"socket\", this.socket);\n }\n\n /**\n * Initialize Socket server\n */\n public async start(): Promise<void> {\n const socketConfig = config.get(\"socket\");\n\n // `this.socket` is only set by boot() once a socket config is present —\n // use it (not a never-assigned field) to detect a successful boot.\n if (!socketConfig || !this.socket) return;\n\n log.success(\"socket\", \"connection\", \"Established Socket.IO server\");\n\n this.active = true;\n }\n\n /**\n * Shutdown Socket server\n *\n * Always closes the Socket.IO layer (and awaits the drain). The underlying\n * node HTTP(S) server is only closed here when this connector created it\n * ({@link ownsRawServer}); when `http` owns the shared server, we leave it\n * for the HTTP connector to drain and close, so the two connectors don't\n * race to `close()` the same socket.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n if (container.has(\"socket\")) {\n const socket = container.get(\"socket\");\n // socket.io's close() takes a callback — await it so shutdown doesn't\n // report done while sockets are still draining.\n await new Promise<void>((resolve) => {\n socket.close(() => resolve());\n });\n }\n\n if (this.ownsRawServer && container.has(\"socket.rawServer\")) {\n const server = container.get(\"socket.rawServer\");\n await new Promise<void>((resolve, reject) => {\n server.close((error?: Error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,8BAA8B;;;;;;;;;;;EAWlC,KAAK;;;;;AAMP,IAAa,kBAAb,cAAqC,cAAc;;;cACX;;;sBASJ,CAAC,sBAAsB;uBAY/B;;;;;CAK1B,MAAa,OAAO;EAClB,MAAM,eAAe,OAAO,IAAI,QAAQ;EAExC,IAAI,CAAC,cAAc;EAInB,IAAI;EACJ,IAAI;GACF,CAAC,CAAE,QAAQ,gBAAiB,MAAM,OAAO;EAC3C,QAAQ;GACN,MAAM,IAAI,MAAM,2BAA2B;EAC7C;EAEA,IAAI,KAAK,UAAU,cAAc,2BAA2B;EAK5D,IAAI;EACJ,IAAI,UAAU,IAAI,aAAa,GAAG;GAEhC,SADgB,UAAU,IAAI,aACf,CAAC,CAAC;GACjB,KAAK,gBAAgB;EACvB,OAAO;GACL,SAAS,aAAa,MAAMA,eAAkB,IAAIC,aAAiB;GACnE,OAAO,OAAO,aAAa,IAAI;GAC/B,KAAK,gBAAgB;EACvB;EAEA,UAAU,IAAI,oBAAoB,MAAM;EAExC,KAAK,SAAS,IAAI,aAAa,QAAQ,aAAa,
|
|
1
|
+
{"version":3,"file":"socket-connector.mjs","names":["createHttpsServer","createHttpServer"],"sources":["../../../../../../../core/src/connectors/socket-connector.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { log } from \"@warlock.js/logger\";\nimport { createServer as createHttpServer } from \"http\";\nimport { createServer as createHttpsServer } from \"https\";\nimport type { Server } from \"socket.io\";\nimport { container } from \"../container\";\nimport { BaseConnector } from \"./base-connector\";\nimport { ConnectorLifecyclePhase, ConnectorName, ConnectorPriority } from \"./types\";\n\n/**\n * Shown when a project enables sockets (a `socket` config is present) but the\n * optional `socket.io` peer is not installed.\n */\nconst SOCKET_INSTALL_INSTRUCTIONS = `\nRealtime socket server requires the socket.io package.\nInstall it with:\n\n warlock add socket\n\nOr manually:\n\n npm install socket.io\n pnpm add socket.io\n yarn add socket.io\n`.trim();\n\n/**\n * Socket Connector\n * Manages Socket server (Socket.IO) lifecycle\n */\nexport class SocketConnector extends BaseConnector {\n public readonly name: ConnectorName = \"socket\";\n public readonly priority = ConnectorPriority.SOCKET;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Files that trigger Socket server restart\n * Note: routes.ts changes will be handled by HMR with wildcard routing\n * Connectors receive config file paths directly (not .env) thanks to layer-executor\n */\n protected readonly watchedFiles = [\"src/config/socket.ts\"];\n\n protected socket?: Server;\n\n /**\n * Whether this connector created the underlying node HTTP(S) server.\n *\n * When `http` is present, socket binds onto the HTTP connector's shared\n * `fastify.server` and must NOT close it on shutdown — the HTTP connector\n * owns the graceful drain. Socket only closes the node server when it\n * created one itself (http absent). See {@link shutdown}.\n */\n protected ownsRawServer = false;\n\n /**\n * Boot the connector\n */\n public async boot() {\n const socketConfig = config.get(\"socket\");\n\n if (!socketConfig) return;\n\n // socket.io is an optional peer — load it lazily so projects that don't use\n // realtime sockets never need it installed (mirrors the mail/storage drivers).\n let SocketServer: typeof import(\"socket.io\").Server;\n try {\n ({ Server: SocketServer } = await import(\"socket.io\"));\n } catch {\n throw new Error(SOCKET_INSTALL_INSTRUCTIONS);\n }\n\n log.info(\"socket\", \"connection\", \"Starting Socket.IO server\");\n\n // now we have two cases\n // 1. http is used, then use it (shared — the HTTP connector owns it)\n // 2. http is not used, then create a new server (we own it)\n let server;\n if (container.has(\"http.server\")) {\n const fastify = container.get(\"http.server\");\n server = fastify.server;\n this.ownsRawServer = false;\n } else {\n server = socketConfig.ssl ? createHttpsServer() : createHttpServer();\n server.listen(socketConfig.port);\n this.ownsRawServer = true;\n }\n\n container.set(\"socket.rawServer\", server);\n\n this.socket = new SocketServer(server, {\n // The shared raw server may carry OTHER `upgrade` consumers — Vite's HMR\n // websocket does exactly that when `WebConnector` is registered\n // (`web/src/server/web-connector.ts`, `server.hmr.server = fastify.server`).\n // engine.io's default `destroyUpgrade: true` schedules a destroy for every\n // upgrade whose path is not `/socket.io`\n // (`engine.io/build/server.js:676-695`: `setTimeout(… socket.end(), 1000)`),\n // and the only thing saving a foreign websocket today is that it wrote its\n // handshake bytes synchronously and so trips the `bytesWritten <= 0` guard\n // before the 1s timer fires. That is an undocumented race, not a contract.\n // Turning the destroy off removes the coupling outright and makes the\n // shared server safe for any app-owned websocket route.\n // A project can still override it via `socket.options`.\n destroyUpgrade: false,\n ...socketConfig.options,\n });\n\n container.set(\"socket\", this.socket);\n }\n\n /**\n * Initialize Socket server\n */\n public async start(): Promise<void> {\n const socketConfig = config.get(\"socket\");\n\n // `this.socket` is only set by boot() once a socket config is present —\n // use it (not a never-assigned field) to detect a successful boot.\n if (!socketConfig || !this.socket) return;\n\n log.success(\"socket\", \"connection\", \"Established Socket.IO server\");\n\n this.active = true;\n }\n\n /**\n * Shutdown Socket server\n *\n * Always closes the Socket.IO layer (and awaits the drain). The underlying\n * node HTTP(S) server is only closed here when this connector created it\n * ({@link ownsRawServer}); when `http` owns the shared server, we leave it\n * for the HTTP connector to drain and close, so the two connectors don't\n * race to `close()` the same socket.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) {\n return;\n }\n\n if (container.has(\"socket\")) {\n const socket = container.get(\"socket\");\n // socket.io's close() takes a callback — await it so shutdown doesn't\n // report done while sockets are still draining.\n await new Promise<void>((resolve) => {\n socket.close(() => resolve());\n });\n }\n\n if (this.ownsRawServer && container.has(\"socket.rawServer\")) {\n const server = container.get(\"socket.rawServer\");\n await new Promise<void>((resolve, reject) => {\n server.close((error?: Error) => {\n if (error) {\n reject(error);\n } else {\n resolve();\n }\n });\n });\n }\n\n this.active = false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,8BAA8B;;;;;;;;;;;EAWlC,KAAK;;;;;AAMP,IAAa,kBAAb,cAAqC,cAAc;;;cACX;;;sBASJ,CAAC,sBAAsB;uBAY/B;;;;;CAK1B,MAAa,OAAO;EAClB,MAAM,eAAe,OAAO,IAAI,QAAQ;EAExC,IAAI,CAAC,cAAc;EAInB,IAAI;EACJ,IAAI;GACF,CAAC,CAAE,QAAQ,gBAAiB,MAAM,OAAO;EAC3C,QAAQ;GACN,MAAM,IAAI,MAAM,2BAA2B;EAC7C;EAEA,IAAI,KAAK,UAAU,cAAc,2BAA2B;EAK5D,IAAI;EACJ,IAAI,UAAU,IAAI,aAAa,GAAG;GAEhC,SADgB,UAAU,IAAI,aACf,CAAC,CAAC;GACjB,KAAK,gBAAgB;EACvB,OAAO;GACL,SAAS,aAAa,MAAMA,eAAkB,IAAIC,aAAiB;GACnE,OAAO,OAAO,aAAa,IAAI;GAC/B,KAAK,gBAAgB;EACvB;EAEA,UAAU,IAAI,oBAAoB,MAAM;EAExC,KAAK,SAAS,IAAI,aAAa,QAAQ;GAarC,gBAAgB;GAChB,GAAG,aAAa;EAClB,CAAC;EAED,UAAU,IAAI,UAAU,KAAK,MAAM;CACrC;;;;CAKA,MAAa,QAAuB;EAKlC,IAAI,CAJiB,OAAO,IAAI,QAIhB,KAAK,CAAC,KAAK,QAAQ;EAEnC,IAAI,QAAQ,UAAU,cAAc,8BAA8B;EAElE,KAAK,SAAS;CAChB;;;;;;;;;;CAWA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QACR;EAGF,IAAI,UAAU,IAAI,QAAQ,GAAG;GAC3B,MAAM,SAAS,UAAU,IAAI,QAAQ;GAGrC,MAAM,IAAI,SAAe,YAAY;IACnC,OAAO,YAAY,QAAQ,CAAC;GAC9B,CAAC;EACH;EAEA,IAAI,KAAK,iBAAiB,UAAU,IAAI,kBAAkB,GAAG;GAC3D,MAAM,SAAS,UAAU,IAAI,kBAAkB;GAC/C,MAAM,IAAI,SAAe,SAAS,WAAW;IAC3C,OAAO,OAAO,UAAkB;KAC9B,IAAI,OACF,OAAO,KAAK;UAEZ,QAAQ;IAEZ,CAAC;GACH,CAAC;EACH;EAEA,KAAK,SAAS;CAChB;AACF"}
|