@tulipes/core 0.1.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/dist/acl/acl.d.ts +49 -0
- package/dist/acl/acl.js +123 -0
- package/dist/acl/acl.js.map +1 -0
- package/dist/acl/index.d.ts +2 -0
- package/dist/acl/index.js +3 -0
- package/dist/acl/index.js.map +1 -0
- package/dist/acl/load-acl.d.ts +11 -0
- package/dist/acl/load-acl.js +38 -0
- package/dist/acl/load-acl.js.map +1 -0
- package/dist/boot/app-files.d.ts +17 -0
- package/dist/boot/app-files.js +31 -0
- package/dist/boot/app-files.js.map +1 -0
- package/dist/boot/banner.d.ts +9 -0
- package/dist/boot/banner.js +82 -0
- package/dist/boot/banner.js.map +1 -0
- package/dist/boot/boot.d.ts +32 -0
- package/dist/boot/boot.js +212 -0
- package/dist/boot/boot.js.map +1 -0
- package/dist/boot/contracts.d.ts +47 -0
- package/dist/boot/contracts.js +2 -0
- package/dist/boot/contracts.js.map +1 -0
- package/dist/boot/import-file.d.ts +7 -0
- package/dist/boot/import-file.js +23 -0
- package/dist/boot/import-file.js.map +1 -0
- package/dist/boot/index.d.ts +3 -0
- package/dist/boot/index.js +2 -0
- package/dist/boot/index.js.map +1 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +7 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/dev.d.ts +8 -0
- package/dist/cli/dev.js +22 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/env-check.d.ts +6 -0
- package/dist/cli/env-check.js +28 -0
- package/dist/cli/env-check.js.map +1 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +45 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/new-module.d.ts +6 -0
- package/dist/cli/new-module.js +84 -0
- package/dist/cli/new-module.js.map +1 -0
- package/dist/cli/sync.d.ts +12 -0
- package/dist/cli/sync.js +117 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/build-config.d.ts +13 -0
- package/dist/config/build-config.js +63 -0
- package/dist/config/build-config.js.map +1 -0
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/database.d.ts +25 -0
- package/dist/db/database.js +111 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/model-store.d.ts +22 -0
- package/dist/db/model-store.js +37 -0
- package/dist/db/model-store.js.map +1 -0
- package/dist/env/coerce.d.ts +17 -0
- package/dist/env/coerce.js +53 -0
- package/dist/env/coerce.js.map +1 -0
- package/dist/env/environment.d.ts +42 -0
- package/dist/env/environment.js +117 -0
- package/dist/env/environment.js.map +1 -0
- package/dist/env/index.d.ts +14 -0
- package/dist/env/index.js +5 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/meta-discovery.d.ts +17 -0
- package/dist/env/meta-discovery.js +65 -0
- package/dist/env/meta-discovery.js.map +1 -0
- package/dist/env/spec.d.ts +133 -0
- package/dist/env/spec.js +63 -0
- package/dist/env/spec.js.map +1 -0
- package/dist/env/variable-store.d.ts +26 -0
- package/dist/env/variable-store.js +49 -0
- package/dist/env/variable-store.js.map +1 -0
- package/dist/errors/boot-error.d.ts +28 -0
- package/dist/errors/boot-error.js +44 -0
- package/dist/errors/boot-error.js.map +1 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/http/error-handler.d.ts +23 -0
- package/dist/http/error-handler.js +48 -0
- package/dist/http/error-handler.js.map +1 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +2 -0
- package/dist/http/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/discovery.d.ts +10 -0
- package/dist/modules/discovery.js +91 -0
- package/dist/modules/discovery.js.map +1 -0
- package/dist/modules/explorer.d.ts +38 -0
- package/dist/modules/explorer.js +64 -0
- package/dist/modules/explorer.js.map +1 -0
- package/dist/modules/graph.d.ts +16 -0
- package/dist/modules/graph.js +80 -0
- package/dist/modules/graph.js.map +1 -0
- package/dist/modules/index.d.ts +4 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/manifest.d.ts +43 -0
- package/dist/modules/manifest.js +26 -0
- package/dist/modules/manifest.js.map +1 -0
- package/dist/queues/index.d.ts +2 -0
- package/dist/queues/index.js +3 -0
- package/dist/queues/index.js.map +1 -0
- package/dist/queues/load-queues.d.ts +12 -0
- package/dist/queues/load-queues.js +56 -0
- package/dist/queues/load-queues.js.map +1 -0
- package/dist/queues/queue-manager.d.ts +44 -0
- package/dist/queues/queue-manager.js +134 -0
- package/dist/queues/queue-manager.js.map +1 -0
- package/dist/sockets/index.d.ts +2 -0
- package/dist/sockets/index.js +3 -0
- package/dist/sockets/index.js.map +1 -0
- package/dist/sockets/load-sockets.d.ts +13 -0
- package/dist/sockets/load-sockets.js +42 -0
- package/dist/sockets/load-sockets.js.map +1 -0
- package/dist/sockets/socket-manager.d.ts +39 -0
- package/dist/sockets/socket-manager.js +85 -0
- package/dist/sockets/socket-manager.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* In dev the app runs straight from TypeScript (via tsx) so conventions
|
|
6
|
+
* resolve to .ts files; a compiled deployment runs .js from dist. Core infers
|
|
7
|
+
* which world it's in from its own file — this very file is only ever .js
|
|
8
|
+
* after a build. The URL alone isn't trustworthy: tsx's tsImport presents
|
|
9
|
+
* modules under a .js URL while only the .ts source exists on disk, so a
|
|
10
|
+
* .js verdict is confirmed against the filesystem.
|
|
11
|
+
*/
|
|
12
|
+
export function detectSourceExtension() {
|
|
13
|
+
const self = fileURLToPath(import.meta.url);
|
|
14
|
+
if (self.endsWith(".ts"))
|
|
15
|
+
return ".ts";
|
|
16
|
+
if (!existsSync(self) && existsSync(self.replace(/\.js$/, ".ts")))
|
|
17
|
+
return ".ts";
|
|
18
|
+
return ".js";
|
|
19
|
+
}
|
|
20
|
+
/** dir name → file suffix, e.g. routes/users.routes.ts */
|
|
21
|
+
const COLLECTIONS = [
|
|
22
|
+
["routeFiles", "routes", ".routes"],
|
|
23
|
+
["modelFiles", "models", ".model"],
|
|
24
|
+
["bootstrapFiles", "bootstrap", ".bootstrap"],
|
|
25
|
+
["queueFiles", "queues", ".queues"],
|
|
26
|
+
["socketFiles", "sockets", ".sockets"],
|
|
27
|
+
];
|
|
28
|
+
export function exploreModules(modules, options = {}) {
|
|
29
|
+
const ext = options.sourceExtension ?? detectSourceExtension();
|
|
30
|
+
return modules.map((module) => {
|
|
31
|
+
const files = {
|
|
32
|
+
module,
|
|
33
|
+
routeFiles: [],
|
|
34
|
+
modelFiles: [],
|
|
35
|
+
bootstrapFiles: [],
|
|
36
|
+
queueFiles: [],
|
|
37
|
+
socketFiles: [],
|
|
38
|
+
};
|
|
39
|
+
const configFile = join(module.dir, `module.config${ext}`);
|
|
40
|
+
if (existsSync(configFile))
|
|
41
|
+
files.configFile = configFile;
|
|
42
|
+
const aclFile = join(module.dir, `module.acl${ext}`);
|
|
43
|
+
if (existsSync(aclFile))
|
|
44
|
+
files.aclFile = aclFile;
|
|
45
|
+
for (const [key, dirName, suffix] of COLLECTIONS) {
|
|
46
|
+
files[key] = listWithSuffix(join(module.dir, dirName), suffix + ext);
|
|
47
|
+
}
|
|
48
|
+
return files;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function listWithSuffix(dir, suffix) {
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return []; // directory absent — the module doesn't use this feature
|
|
58
|
+
}
|
|
59
|
+
return entries
|
|
60
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(suffix))
|
|
61
|
+
.map((entry) => join(dir, entry.name))
|
|
62
|
+
.sort(); // deterministic order within a module
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=explorer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"explorer.js","sourceRoot":"","sources":["../../src/modules/explorer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiBzC;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,IAAI,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAChF,OAAO,KAAK,CAAC;AACf,CAAC;AAeD,0DAA0D;AAC1D,MAAM,WAAW,GAAG;IAClB,CAAC,YAAY,EAAE,QAAQ,EAAE,SAAS,CAAC;IACnC,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ,CAAC;IAClC,CAAC,gBAAgB,EAAE,WAAW,EAAE,YAAY,CAAC;IAC7C,CAAC,YAAY,EAAE,QAAQ,EAAE,SAAS,CAAC;IACnC,CAAC,aAAa,EAAE,SAAS,EAAE,UAAU,CAAC;CAC9B,CAAC;AAOX,MAAM,UAAU,cAAc,CAC5B,OAAkC,EAClC,OAAO,GAAmB,EAAE;IAE5B,MAAM,GAAG,GAAG,OAAO,CAAC,eAAe,IAAI,qBAAqB,EAAE,CAAC;IAE/D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAgB;YACzB,MAAM;YACN,UAAU,EAAE,EAAE;YACd,UAAU,EAAE,EAAE;YACd,cAAc,EAAE,EAAE;YAClB,UAAU,EAAE,EAAE;YACd,WAAW,EAAE,EAAE;SAChB,CAAC;QAEF,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,gBAAgB,GAAG,EAAE,CAAC,CAAC;QAC3D,IAAI,UAAU,CAAC,UAAU,CAAC;YAAE,KAAK,CAAC,UAAU,GAAG,UAAU,CAAC;QAE1D,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,aAAa,GAAG,EAAE,CAAC,CAAC;QACrD,IAAI,UAAU,CAAC,OAAO,CAAC;YAAE,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;QAEjD,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;YACjD,KAAK,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,CAAC;QACvE,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,GAAW,EAAE,MAAc;IACjD,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,yDAAyD;IACtE,CAAC;IACD,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SAChE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;SACrC,IAAI,EAAE,CAAC,CAAC,sCAAsC;AACnD,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import type { ModuleManifest } from "./manifest.js";
|
|
3
|
+
/**
|
|
4
|
+
* Compute the definitive load order. Three sort keys, applied in order:
|
|
5
|
+
*
|
|
6
|
+
* 1. tier — "sys" before "app"
|
|
7
|
+
* 2. priority — lower first, tie-breaker within a tier
|
|
8
|
+
* 3. dependsOn — topological sort within the resulting order
|
|
9
|
+
*
|
|
10
|
+
* The topo sort is Kahn's algorithm, but ready modules are always picked in
|
|
11
|
+
* (tier, priority, name) order — that keeps the output deterministic AND
|
|
12
|
+
* guarantees the tier grouping holds: since a sys module may never depend on
|
|
13
|
+
* an app module, every sys module drains from the ready set before the first
|
|
14
|
+
* app module is picked.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sortModules(modules: readonly ModuleManifest[], report: BootReport): ModuleManifest[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute the definitive load order. Three sort keys, applied in order:
|
|
4
|
+
*
|
|
5
|
+
* 1. tier — "sys" before "app"
|
|
6
|
+
* 2. priority — lower first, tie-breaker within a tier
|
|
7
|
+
* 3. dependsOn — topological sort within the resulting order
|
|
8
|
+
*
|
|
9
|
+
* The topo sort is Kahn's algorithm, but ready modules are always picked in
|
|
10
|
+
* (tier, priority, name) order — that keeps the output deterministic AND
|
|
11
|
+
* guarantees the tier grouping holds: since a sys module may never depend on
|
|
12
|
+
* an app module, every sys module drains from the ready set before the first
|
|
13
|
+
* app module is picked.
|
|
14
|
+
*/
|
|
15
|
+
export function sortModules(modules, report) {
|
|
16
|
+
const byName = new Map(modules.map((m) => [m.name, m]));
|
|
17
|
+
let valid = true;
|
|
18
|
+
for (const module of modules) {
|
|
19
|
+
for (const dep of module.dependsOn) {
|
|
20
|
+
const target = byName.get(dep);
|
|
21
|
+
if (!target) {
|
|
22
|
+
report.add({
|
|
23
|
+
scope: "modules",
|
|
24
|
+
module: module.name,
|
|
25
|
+
message: `dependsOn "${dep}" — no such module exists`,
|
|
26
|
+
});
|
|
27
|
+
valid = false;
|
|
28
|
+
}
|
|
29
|
+
else if (module.tier === "sys" && target.tier === "app") {
|
|
30
|
+
report.add({
|
|
31
|
+
scope: "modules",
|
|
32
|
+
module: module.name,
|
|
33
|
+
message: `sys module cannot depend on app module "${dep}" — sys loads first by definition`,
|
|
34
|
+
});
|
|
35
|
+
valid = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Broken edges make any order meaningless; skip the sort, the report
|
|
40
|
+
// already carries the reason boot will abort.
|
|
41
|
+
if (!valid)
|
|
42
|
+
return [];
|
|
43
|
+
const presorted = [...modules].sort((a, b) => Number(a.tier === "app") - Number(b.tier === "app") ||
|
|
44
|
+
a.priority - b.priority ||
|
|
45
|
+
a.name.localeCompare(b.name));
|
|
46
|
+
const blockers = new Map(modules.map((m) => [m.name, new Set(m.dependsOn)]));
|
|
47
|
+
const order = [];
|
|
48
|
+
const placed = new Set();
|
|
49
|
+
while (order.length < modules.length) {
|
|
50
|
+
const next = presorted.find((m) => !placed.has(m.name) && blockers.get(m.name).size === 0);
|
|
51
|
+
if (!next) {
|
|
52
|
+
report.add({
|
|
53
|
+
scope: "modules",
|
|
54
|
+
message: `dependency cycle: ${describeCycle(modules, blockers)}`,
|
|
55
|
+
});
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
order.push(next);
|
|
59
|
+
placed.add(next.name);
|
|
60
|
+
for (const remaining of blockers.values())
|
|
61
|
+
remaining.delete(next.name);
|
|
62
|
+
}
|
|
63
|
+
return order;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* When Kahn stalls, every unplaced module still has a blocker — walking
|
|
67
|
+
* first-blockers from any of them must eventually revisit a node. That loop
|
|
68
|
+
* is the cycle we print (e.g. "auth → users → auth").
|
|
69
|
+
*/
|
|
70
|
+
function describeCycle(modules, blockers) {
|
|
71
|
+
const start = modules.find((m) => blockers.get(m.name).size > 0).name;
|
|
72
|
+
const path = [];
|
|
73
|
+
let current = start;
|
|
74
|
+
while (!path.includes(current)) {
|
|
75
|
+
path.push(current);
|
|
76
|
+
current = blockers.get(current).values().next().value;
|
|
77
|
+
}
|
|
78
|
+
return [...path.slice(path.indexOf(current)), current].join(" → ");
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/modules/graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { discoverModules } from "./discovery.js";
|
|
2
|
+
export { detectSourceExtension, exploreModules, type ExploreOptions, type ModuleFiles, type SourceExtension, } from "./explorer.js";
|
|
3
|
+
export { sortModules } from "./graph.js";
|
|
4
|
+
export { DEFAULT_PRIORITY, tulipesManifestSchema, moduleTiers, type ModuleManifest, type ModuleTier, } from "./manifest.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { discoverModules } from "./discovery.js";
|
|
2
|
+
export { detectSourceExtension, exploreModules, } from "./explorer.js";
|
|
3
|
+
export { sortModules } from "./graph.js";
|
|
4
|
+
export { DEFAULT_PRIORITY, tulipesManifestSchema, moduleTiers, } from "./manifest.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/modules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EACL,qBAAqB,EACrB,cAAc,GAIf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,WAAW,GAGZ,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A module's ordering metadata lives under the "tulipes" key of its
|
|
4
|
+
* package.json — never in the folder name — so reprioritizing a module can
|
|
5
|
+
* never break the `dependsOn` references pointing at it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const moduleTiers: readonly [
|
|
8
|
+
/** Framework-critical modules; always load before every "app" module. */
|
|
9
|
+
"sys", "app"];
|
|
10
|
+
export type ModuleTier = (typeof moduleTiers)[number];
|
|
11
|
+
export declare const DEFAULT_PRIORITY = 100;
|
|
12
|
+
/**
|
|
13
|
+
* `.strict()` so a typo like "dependson" crashes boot instead of silently
|
|
14
|
+
* loading the module with default ordering.
|
|
15
|
+
*/
|
|
16
|
+
export declare const tulipesManifestSchema: z.ZodObject<{
|
|
17
|
+
tier: z.ZodDefault<z.ZodEnum<["sys", "app"]>>;
|
|
18
|
+
/** Lower loads earlier; tie-breaker inside a tier. */
|
|
19
|
+
priority: z.ZodDefault<z.ZodNumber>;
|
|
20
|
+
/** Short names of modules that must load before this one. */
|
|
21
|
+
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
22
|
+
}, "strict", z.ZodTypeAny, {
|
|
23
|
+
tier: "app" | "sys";
|
|
24
|
+
priority: number;
|
|
25
|
+
dependsOn: string[];
|
|
26
|
+
}, {
|
|
27
|
+
tier?: "app" | "sys" | undefined;
|
|
28
|
+
priority?: number | undefined;
|
|
29
|
+
dependsOn?: string[] | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
export interface ModuleManifest {
|
|
32
|
+
/**
|
|
33
|
+
* Canonical short name — the last segment of the package name
|
|
34
|
+
* (`@app/users` → `users`). This is what `dependsOn` references.
|
|
35
|
+
*/
|
|
36
|
+
name: string;
|
|
37
|
+
packageName: string;
|
|
38
|
+
/** Absolute path to the module's folder. */
|
|
39
|
+
dir: string;
|
|
40
|
+
tier: ModuleTier;
|
|
41
|
+
priority: number;
|
|
42
|
+
dependsOn: readonly string[];
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A module's ordering metadata lives under the "tulipes" key of its
|
|
4
|
+
* package.json — never in the folder name — so reprioritizing a module can
|
|
5
|
+
* never break the `dependsOn` references pointing at it.
|
|
6
|
+
*/
|
|
7
|
+
export const moduleTiers = [
|
|
8
|
+
/** Framework-critical modules; always load before every "app" module. */
|
|
9
|
+
"sys",
|
|
10
|
+
"app",
|
|
11
|
+
];
|
|
12
|
+
export const DEFAULT_PRIORITY = 100;
|
|
13
|
+
/**
|
|
14
|
+
* `.strict()` so a typo like "dependson" crashes boot instead of silently
|
|
15
|
+
* loading the module with default ordering.
|
|
16
|
+
*/
|
|
17
|
+
export const tulipesManifestSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
tier: z.enum(moduleTiers).default("app"),
|
|
20
|
+
/** Lower loads earlier; tie-breaker inside a tier. */
|
|
21
|
+
priority: z.number().int().default(DEFAULT_PRIORITY),
|
|
22
|
+
/** Short names of modules that must load before this one. */
|
|
23
|
+
dependsOn: z.array(z.string().min(1)).default([]),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/modules/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AAEH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,yEAAyE;IACzE,KAAK;IACL,KAAK;CACG,CAAC;AAIX,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAEpC;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC;KACnC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACxC,sDAAsD;IACtD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC;IACpD,6DAA6D;IAC7D,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAClD,CAAC;KACD,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/queues/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,UAAU,EAAiB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import type { Ctx } from "../boot/contracts.js";
|
|
3
|
+
import type { ModuleFiles } from "../modules/explorer.js";
|
|
4
|
+
import { QueueManager, type QueueRegistry } from "./queue-manager.js";
|
|
5
|
+
/** Default export of `queues/*.queues.ts`. */
|
|
6
|
+
export type QueuesFn = (ctx: Ctx, queues: QueueRegistry) => void | Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Phase 9: run every `*.queues.ts` in load order. Same skip/refuse logic as
|
|
9
|
+
* the database: a queue-less app is fine, queue files without REDIS_URL are
|
|
10
|
+
* a contradiction.
|
|
11
|
+
*/
|
|
12
|
+
export declare function initQueues(explored: readonly ModuleFiles[], ctx: Ctx, report: BootReport): Promise<QueueManager | undefined>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import { importFile } from "../boot/import-file.js";
|
|
3
|
+
import { QueueManager } from "./queue-manager.js";
|
|
4
|
+
/** Well-known infra variable, declared by the app's sys core module. */
|
|
5
|
+
const REDIS_URL = "REDIS_URL";
|
|
6
|
+
/**
|
|
7
|
+
* Phase 9: run every `*.queues.ts` in load order. Same skip/refuse logic as
|
|
8
|
+
* the database: a queue-less app is fine, queue files without REDIS_URL are
|
|
9
|
+
* a contradiction.
|
|
10
|
+
*/
|
|
11
|
+
export async function initQueues(explored, ctx, report) {
|
|
12
|
+
const hasQueueFiles = explored.some((files) => files.queueFiles.length > 0);
|
|
13
|
+
const url = ctx.Environment.store.has(REDIS_URL)
|
|
14
|
+
? String(ctx.Environment.get(REDIS_URL))
|
|
15
|
+
: undefined;
|
|
16
|
+
if (!url) {
|
|
17
|
+
if (hasQueueFiles) {
|
|
18
|
+
report.add({
|
|
19
|
+
scope: "boot",
|
|
20
|
+
message: `modules declare queues but "${REDIS_URL}" is not declared/set — redis is required`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
if (!hasQueueFiles)
|
|
26
|
+
return undefined;
|
|
27
|
+
const manager = new QueueManager(url);
|
|
28
|
+
ctx.queues = manager;
|
|
29
|
+
for (const { module, queueFiles } of explored) {
|
|
30
|
+
for (const file of queueFiles) {
|
|
31
|
+
const exported = await importFile(file, "boot", module.name, report);
|
|
32
|
+
if (!exported)
|
|
33
|
+
continue;
|
|
34
|
+
if (typeof exported.default !== "function") {
|
|
35
|
+
report.add({
|
|
36
|
+
scope: "boot",
|
|
37
|
+
module: module.name,
|
|
38
|
+
message: `${file} must default-export a queues function (ctx, queues) => void`,
|
|
39
|
+
});
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await exported.default(ctx, manager.forModule(module.name, report));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
report.add({
|
|
47
|
+
scope: "boot",
|
|
48
|
+
module: module.name,
|
|
49
|
+
message: `queues function threw: ${error.message}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return manager;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=load-queues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-queues.js","sourceRoot":"","sources":["../../src/queues/load-queues.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGpD,OAAO,EAAE,YAAY,EAAsB,MAAM,oBAAoB,CAAC;AAKtE,wEAAwE;AACxE,MAAM,SAAS,GAAG,WAAW,CAAC;AAE9B;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,QAAgC,EAChC,GAAQ,EACR,MAAkB;IAElB,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;QAC9C,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxC,CAAC,CAAC,SAAS,CAAC;IAEd,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,CAAC,GAAG,CAAC;gBACT,KAAK,EAAE,MAAM;gBACb,OAAO,EAAE,+BAA+B,SAAS,2CAA2C;aAC7F,CAAC,CAAC;QACL,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,aAAa;QAAE,OAAO,SAAS,CAAC;IAErC,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,GAAG,CAAC,CAAC;IACtC,GAAG,CAAC,MAAM,GAAG,OAAO,CAAC;IAErB,KAAK,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC9C,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,QAAQ,GAAG,MAAM,UAAU,CAC/B,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAClC,CAAC;YACF,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAExB,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC3C,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,OAAO,EAAE,GAAG,IAAI,8DAA8D;iBAC/E,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,IAAI,CAAC;gBACH,MAAO,QAAQ,CAAC,OAAoB,CAClC,GAAG,EACH,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CACvC,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,OAAO,EAAE,0BAA2B,KAAe,CAAC,OAAO,EAAE;iBAC9D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Queue, type JobsOptions, type Processor, type QueueOptions, type WorkerOptions } from "bullmq";
|
|
2
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
3
|
+
/**
|
|
4
|
+
* One manager owns every queue in the process. The same `*.queues.ts` files
|
|
5
|
+
* run in BOTH modes — definitions and processors are always registered — and
|
|
6
|
+
* the mode decides what happens with them:
|
|
7
|
+
*
|
|
8
|
+
* web → queues produce; processors are registered but never started
|
|
9
|
+
* worker → `startWorkers()` turns every registered processor into a
|
|
10
|
+
* consuming BullMQ Worker
|
|
11
|
+
*
|
|
12
|
+
* That symmetry is what lets a module describe its background work once.
|
|
13
|
+
*/
|
|
14
|
+
export declare class QueueManager {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(redisUrl: string);
|
|
17
|
+
/** @internal Module-scoped facade for the loader. */
|
|
18
|
+
forModule(moduleName: string, report: BootReport): QueueRegistry;
|
|
19
|
+
/** @internal QueueRegistry only. */
|
|
20
|
+
define_(name: string, owner: string, options?: Partial<QueueOptions>): string | undefined;
|
|
21
|
+
/** @internal QueueRegistry only. */
|
|
22
|
+
process_(name: string, handler: Processor, owner: string, options?: Partial<WorkerOptions>): string | undefined;
|
|
23
|
+
/** Introspection for the startup banner and tooling. */
|
|
24
|
+
names(): string[];
|
|
25
|
+
get processorCount(): number;
|
|
26
|
+
get workerCount(): number;
|
|
27
|
+
/** Produce a job. The queue must have been defined by some module. */
|
|
28
|
+
add(queueName: string, jobName: string, data: unknown, options?: JobsOptions): Promise<void>;
|
|
29
|
+
queue(name: string): Queue;
|
|
30
|
+
/** Worker mode only — called by the boot driver, never by modules. */
|
|
31
|
+
startWorkers(report: BootReport): number;
|
|
32
|
+
/** Drain order: stop consuming, then producers, then raw connections. */
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Module-scoped facade handed to `*.queues.ts` — mirrors AclBuilder: every
|
|
37
|
+
* registration records its module so conflicts crash with both names.
|
|
38
|
+
*/
|
|
39
|
+
export declare class QueueRegistry {
|
|
40
|
+
#private;
|
|
41
|
+
constructor(manager: QueueManager, moduleName: string, report: BootReport);
|
|
42
|
+
define(name: string, options?: Partial<QueueOptions>): this;
|
|
43
|
+
process(name: string, handler: Processor, options?: Partial<WorkerOptions>): this;
|
|
44
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Queue, Worker, } from "bullmq";
|
|
2
|
+
import { Redis } from "ioredis";
|
|
3
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
4
|
+
/**
|
|
5
|
+
* One manager owns every queue in the process. The same `*.queues.ts` files
|
|
6
|
+
* run in BOTH modes — definitions and processors are always registered — and
|
|
7
|
+
* the mode decides what happens with them:
|
|
8
|
+
*
|
|
9
|
+
* web → queues produce; processors are registered but never started
|
|
10
|
+
* worker → `startWorkers()` turns every registered processor into a
|
|
11
|
+
* consuming BullMQ Worker
|
|
12
|
+
*
|
|
13
|
+
* That symmetry is what lets a module describe its background work once.
|
|
14
|
+
*/
|
|
15
|
+
export class QueueManager {
|
|
16
|
+
#url;
|
|
17
|
+
#queues = new Map();
|
|
18
|
+
#processors = new Map();
|
|
19
|
+
#workers = [];
|
|
20
|
+
#connections = [];
|
|
21
|
+
constructor(redisUrl) {
|
|
22
|
+
this.#url = redisUrl;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Every BullMQ client gets its own connection: workers need dedicated
|
|
26
|
+
* blocking connections anyway, and never sharing keeps close() reasoning
|
|
27
|
+
* trivial. maxRetriesPerRequest: null is a BullMQ requirement.
|
|
28
|
+
*/
|
|
29
|
+
#connect() {
|
|
30
|
+
const connection = new Redis(this.#url, { maxRetriesPerRequest: null });
|
|
31
|
+
this.#connections.push(connection);
|
|
32
|
+
return connection;
|
|
33
|
+
}
|
|
34
|
+
/** @internal Module-scoped facade for the loader. */
|
|
35
|
+
forModule(moduleName, report) {
|
|
36
|
+
return new QueueRegistry(this, moduleName, report);
|
|
37
|
+
}
|
|
38
|
+
/** @internal QueueRegistry only. */
|
|
39
|
+
define_(name, owner, options) {
|
|
40
|
+
const existing = this.#queues.get(name);
|
|
41
|
+
if (existing)
|
|
42
|
+
return existing.owner;
|
|
43
|
+
const queue = new Queue(name, { ...options, connection: this.#connect() });
|
|
44
|
+
this.#queues.set(name, { queue, owner });
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** @internal QueueRegistry only. */
|
|
48
|
+
process_(name, handler, owner, options) {
|
|
49
|
+
const existing = this.#processors.get(name);
|
|
50
|
+
if (existing)
|
|
51
|
+
return existing.owner;
|
|
52
|
+
this.#processors.set(name, { handler, owner, options });
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Introspection for the startup banner and tooling. */
|
|
56
|
+
names() {
|
|
57
|
+
return [...this.#queues.keys()];
|
|
58
|
+
}
|
|
59
|
+
get processorCount() {
|
|
60
|
+
return this.#processors.size;
|
|
61
|
+
}
|
|
62
|
+
get workerCount() {
|
|
63
|
+
return this.#workers.length;
|
|
64
|
+
}
|
|
65
|
+
/** Produce a job. The queue must have been defined by some module. */
|
|
66
|
+
async add(queueName, jobName, data, options) {
|
|
67
|
+
await this.queue(queueName).add(jobName, data, options);
|
|
68
|
+
}
|
|
69
|
+
queue(name) {
|
|
70
|
+
const stored = this.#queues.get(name);
|
|
71
|
+
if (!stored) {
|
|
72
|
+
throw new Error(`Queue "${name}" was never defined by any module (known: ${[...this.#queues.keys()].join(", ") || "none"})`);
|
|
73
|
+
}
|
|
74
|
+
return stored.queue;
|
|
75
|
+
}
|
|
76
|
+
/** Worker mode only — called by the boot driver, never by modules. */
|
|
77
|
+
startWorkers(report) {
|
|
78
|
+
for (const [name, { handler, owner, options }] of this.#processors) {
|
|
79
|
+
if (!this.#queues.has(name)) {
|
|
80
|
+
report.add({
|
|
81
|
+
scope: "boot",
|
|
82
|
+
module: owner,
|
|
83
|
+
message: `processor registered for undefined queue "${name}" — call define() first`,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
this.#workers.push(new Worker(name, handler, { ...options, connection: this.#connect() }));
|
|
88
|
+
}
|
|
89
|
+
return this.#workers.length;
|
|
90
|
+
}
|
|
91
|
+
/** Drain order: stop consuming, then producers, then raw connections. */
|
|
92
|
+
async close() {
|
|
93
|
+
await Promise.all(this.#workers.map((worker) => worker.close()));
|
|
94
|
+
await Promise.all([...this.#queues.values()].map(({ queue }) => queue.close()));
|
|
95
|
+
await Promise.all(this.#connections.map((connection) => connection.quit().catch(() => { })));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// BullMQ reserves ":" (its redis key separator), so module namespacing in
|
|
99
|
+
// queue names uses "." instead: "users.welcome".
|
|
100
|
+
const QUEUE_NAME = /^[a-z0-9-]+(\.[a-z0-9-]+)?$/;
|
|
101
|
+
/**
|
|
102
|
+
* Module-scoped facade handed to `*.queues.ts` — mirrors AclBuilder: every
|
|
103
|
+
* registration records its module so conflicts crash with both names.
|
|
104
|
+
*/
|
|
105
|
+
export class QueueRegistry {
|
|
106
|
+
#manager;
|
|
107
|
+
#module;
|
|
108
|
+
#report;
|
|
109
|
+
constructor(manager, moduleName, report) {
|
|
110
|
+
this.#manager = manager;
|
|
111
|
+
this.#module = moduleName;
|
|
112
|
+
this.#report = report;
|
|
113
|
+
}
|
|
114
|
+
define(name, options) {
|
|
115
|
+
if (!QUEUE_NAME.test(name)) {
|
|
116
|
+
this.#issue(`queue name "${name}" is invalid — lowercase kebab-case, optional "." namespace`);
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
const owner = this.#manager.define_(name, this.#module, options);
|
|
120
|
+
if (owner)
|
|
121
|
+
this.#issue(`queue "${name}" is already defined by module "${owner}"`);
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
process(name, handler, options) {
|
|
125
|
+
const owner = this.#manager.process_(name, handler, this.#module, options);
|
|
126
|
+
if (owner)
|
|
127
|
+
this.#issue(`queue "${name}" already has a processor from module "${owner}"`);
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
#issue(message) {
|
|
131
|
+
this.#report.add({ scope: "boot", module: this.#module, message });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=queue-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queue-manager.js","sourceRoot":"","sources":["../../src/queues/queue-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sockets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Server as HttpServer } from "node:http";
|
|
2
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
3
|
+
import type { Ctx } from "../boot/contracts.js";
|
|
4
|
+
import type { ModuleFiles } from "../modules/explorer.js";
|
|
5
|
+
import { SocketManager, type SocketRegistry } from "./socket-manager.js";
|
|
6
|
+
/** Default export of `sockets/*.sockets.ts` (web branch only). */
|
|
7
|
+
export type SocketsFn = (ctx: Ctx, sockets: SocketRegistry) => void | Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Phase 11w: attach Socket.IO to the http server and run every
|
|
10
|
+
* `*.sockets.ts` in load order. No socket files → no Socket.IO server at
|
|
11
|
+
* all; websockets are opt-in per app, not a standing cost.
|
|
12
|
+
*/
|
|
13
|
+
export declare function initSockets(explored: readonly ModuleFiles[], ctx: Ctx, httpServer: HttpServer, report: BootReport): Promise<SocketManager | undefined>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { BootReport } from "../errors/boot-error.js";
|
|
2
|
+
import { importFile } from "../boot/import-file.js";
|
|
3
|
+
import { SocketManager } from "./socket-manager.js";
|
|
4
|
+
/**
|
|
5
|
+
* Phase 11w: attach Socket.IO to the http server and run every
|
|
6
|
+
* `*.sockets.ts` in load order. No socket files → no Socket.IO server at
|
|
7
|
+
* all; websockets are opt-in per app, not a standing cost.
|
|
8
|
+
*/
|
|
9
|
+
export async function initSockets(explored, ctx, httpServer, report) {
|
|
10
|
+
if (!explored.some((files) => files.socketFiles.length > 0)) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const manager = new SocketManager(httpServer);
|
|
14
|
+
ctx.sockets = manager;
|
|
15
|
+
for (const { module, socketFiles } of explored) {
|
|
16
|
+
for (const file of socketFiles) {
|
|
17
|
+
const exported = await importFile(file, "boot", module.name, report);
|
|
18
|
+
if (!exported)
|
|
19
|
+
continue;
|
|
20
|
+
if (typeof exported.default !== "function") {
|
|
21
|
+
report.add({
|
|
22
|
+
scope: "boot",
|
|
23
|
+
module: module.name,
|
|
24
|
+
message: `${file} must default-export a sockets function (ctx, sockets) => void`,
|
|
25
|
+
});
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await exported.default(ctx, manager.forModule(module.name, report));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
report.add({
|
|
33
|
+
scope: "boot",
|
|
34
|
+
module: module.name,
|
|
35
|
+
message: `sockets function threw: ${error.message}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return manager;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=load-sockets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-sockets.js","sourceRoot":"","sources":["../../src/sockets/load-sockets.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGpD,OAAO,EAAE,aAAa,EAAuB,MAAM,qBAAqB,CAAC;AAKzE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAgC,EAChC,GAAQ,EACR,UAAsB,EACtB,MAAkB;IAElB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,UAAU,CAAC,CAAC;IAC9C,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;IAEtB,KAAK,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC/C,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;YAC/B,MAAM,QAAQ,GAAG,MAAM,UAAU,CAC/B,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAClC,CAAC;YACF,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAExB,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC3C,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,OAAO,EAAE,GAAG,IAAI,gEAAgE;iBACjF,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,IAAI,CAAC;gBACH,MAAO,QAAQ,CAAC,OAAqB,CACnC,GAAG,EACH,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CACvC,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,MAAM;oBACb,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,OAAO,EAAE,2BAA4B,KAAe,CAAC,OAAO,EAAE;iBAC/D,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
|