Skip to content

Commit 5b73175

Browse files
committed
Add and use template lookup cache
Well, yes, this is all JS style code. But it gets the job done :-)
1 parent b96a51a commit 5b73175

File tree

3 files changed

+109
-63
lines changed

3 files changed

+109
-63
lines changed

Sources/express/Express.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import protocol MacroCore.EnvironmentKey
1313
import struct MacroCore.EnvironmentValues
1414
import class http.IncomingMessage
1515
import class http.ServerResponse
16+
import NIOConcurrencyHelpers
1617

1718
/**
1819
* # The Express application object
@@ -75,6 +76,15 @@ import class http.ServerResponse
7576
* application.
7677
* The neat thing is that the routes used within the admin application are then
7778
* relative to "/admin", e.g. "/admin/index" for a route targetting "/index".
79+
*
80+
* ## Thread Safety
81+
*
82+
* Generally application setup has to be done before the stack is activated
83+
* (e.g. listen is called). It generally is *not* thread safe after startup.
84+
* I.e. routes, engines, settings cannot be added or modified.
85+
*
86+
* Exception: The eventloop count is set to 1, in this case everything goes,
87+
* just like in Node.js.
7888
*/
7989
open class Express: SettingsHolder, MountableMiddlewareObject, MiddlewareObject,
8090
RouteKeeper
@@ -101,9 +111,12 @@ open class Express: SettingsHolder, MountableMiddlewareObject, MiddlewareObject,
101111

102112
// defaults
103113
set("view engine", "mustache")
104-
105-
if let env = process.env["EXPRESS_ENV"], !env.isEmpty {
106-
set("env", env)
114+
set("view", Express.View.self)
115+
116+
let env = settings.env.lowercased() // this does the lookup
117+
set("env", env) // which we want to cache
118+
if env == "production" {
119+
enable("view cache")
107120
}
108121
}
109122

@@ -186,10 +199,11 @@ open class Express: SettingsHolder, MountableMiddlewareObject, MiddlewareObject,
186199
public func get(_ key: String) -> Any? {
187200
return settingsStore[key]
188201
}
189-
190-
202+
191203
// MARK: - Engines
192204

205+
let viewCache = NIOLockedValueBox([ String : View ]())
206+
193207
/// Extension to engine, e.g. ".mustache" to MustacheEngine
194208
private(set) var engines = [ String : ExpressEngine ]()
195209

Sources/express/Render.swift

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,100 @@ public extension ServerResponse {
5858

5959
public extension Express {
6060

61+
/**
62+
* Locate the rendering engine for a given path and render it with the options
63+
* that are passed in.
64+
*
65+
* Refer to the ``ServerResponse/render`` method for details.
66+
*
67+
* - Parameters:
68+
* - path: the filesystem path to a template.
69+
* - options: Any options passed to the rendering engine.
70+
* - res: The response the rendering will be sent to.
71+
*/
72+
func render(path: String, options: Any?, to res: ServerResponse) {
73+
let log = self.log
74+
let ext = fs.path.extname(path)
75+
let viewEngine = ext.isEmpty ? defaultEngine : ext
76+
77+
guard let engine = engines[viewEngine] else {
78+
log.error("Did not find view engine for extension: \(viewEngine)")
79+
res.emit(error: ExpressRenderingError.unsupportedViewEngine(viewEngine))
80+
res.finishRender500IfNecessary()
81+
return
82+
}
83+
84+
engine(path, options) { ( results: Any?... ) in
85+
let rc = results.count
86+
let v0 = rc > 0 ? results[0] : nil
87+
let v1 = rc > 1 ? results[1] : nil
88+
89+
if let error = v0 {
90+
res.emit(error: ExpressRenderingError
91+
.templateError(error as? Swift.Error))
92+
log.error("template error:", error)
93+
res.writeHead(500)
94+
res.end()
95+
return
96+
}
97+
98+
guard let result = v1 else { // Hm?
99+
log.warn("template returned no content: \(path) \(results)")
100+
res.writeHead(204)
101+
res.end()
102+
return
103+
}
104+
105+
// TBD: maybe support a stream as a result? (result.pipe(res))
106+
// Or generators, there are many more options.
107+
if !(result is String) {
108+
log.warn("template rendering result is not a String:", result)
109+
}
110+
111+
let s = (result as? String) ?? "\(result)"
112+
113+
// Wow, this is harder than it looks when we want to consider a MIMEType
114+
// object as a value :-)
115+
var setContentType = true
116+
if let oldType = res.getHeader("Content-Type") {
117+
let s = (oldType as? String) ?? String(describing: oldType) // FIXME
118+
setContentType = (s == "httpd/unix-directory") // a hack for Apache
119+
}
120+
121+
if setContentType {
122+
// FIXME: also consider extension of template (.html, .vcf etc)
123+
res.setHeader("Content-Type", detectTypeForContent(string: s))
124+
}
125+
126+
res.writeHead(200)
127+
res.write(s)
128+
res.end()
129+
}
130+
}
131+
61132
/**
62133
* Lookup a template with the given name, locate the rendering engine for it,
63134
* and render it with the options that are passed in.
64135
*
65136
* Refer to the ``ServerResponse/render`` method for details.
66137
*/
67138
func render(template: String, options: Any?, to res: ServerResponse) {
68-
let log = self.log
69-
70-
let defaultEngine = self.defaultEngine
139+
let log = self.log
140+
let cacheOn = settings.enabled("view cache")
71141
let emptyOpts : [ String : Any ] = [:]
72142
let appViewOptions = get("view options") ?? emptyOpts // Any?
73143
let viewOptions = options ?? appViewOptions // TODO: merge if possible
74144
// not usually possible, because not guaranteed to be dicts!
75145

76-
let view = View(name: template, options: self)
146+
if cacheOn, let view = viewCache.withLockedValue({ $0[template] }),
147+
let path = view.path
148+
{
149+
log.trace("Using cached view:", template)
150+
return self.render(path: path, options: viewOptions, to: res)
151+
}
152+
153+
let viewType : View.Type = (get("view") as? View.Type) ?? View.self
154+
let view = viewType.init(name: template, options: self)
77155
let name = path.basename(template, path.extname(template))
78156
view.lookup(name) { pathOrNot in
79157
guard let path = pathOrNot else {
@@ -82,61 +160,15 @@ public extension Express {
82160
return
83161
}
84162

85-
let ext = fs.path.extname(path)
86-
let viewEngine = ext.isEmpty ? defaultEngine : ext
87-
guard let engine = self.engines[viewEngine] else {
88-
log.error("Did not find view engine for extension: \(viewEngine)")
89-
res.emit(error: ExpressRenderingError.unsupportedViewEngine(viewEngine))
90-
res.finishRender500IfNecessary()
91-
return
92-
}
93-
94-
engine(path, viewOptions) { ( results: Any?... ) in
95-
let rc = results.count
96-
let v0 = rc > 0 ? results[0] : nil
97-
let v1 = rc > 1 ? results[1] : nil
98-
99-
if let error = v0 {
100-
res.emit(error: ExpressRenderingError
101-
.templateError(error as? Swift.Error))
102-
log.error("template error:", error)
103-
res.writeHead(500)
104-
res.end()
105-
return
106-
}
107-
108-
guard let result = v1 else { // Hm?
109-
log.warn("template returned no content: \(template) \(results)")
110-
res.writeHead(204)
111-
res.end()
112-
return
113-
}
114-
115-
// TBD: maybe support a stream as a result? (result.pipe(res))
116-
// Or generators, there are many more options.
117-
if !(result is String) {
118-
log.warn("template rendering result is not a String:", result)
119-
}
120-
121-
let s = (result as? String) ?? "\(result)"
122-
123-
// Wow, this is harder than it looks when we want to consider a MIMEType
124-
// object as a value :-)
125-
var setContentType = true
126-
if let oldType = res.getHeader("Content-Type") {
127-
let s = (oldType as? String) ?? String(describing: oldType) // FIXME
128-
setContentType = (s == "httpd/unix-directory") // a hack for Apache
129-
}
130-
131-
if setContentType {
132-
// FIXME: also consider extension of template (.html, .vcf etc)
133-
res.setHeader("Content-Type", detectTypeForContent(string: s))
163+
view.path = path // cache path
164+
if cacheOn {
165+
log.trace("Caching view:", template)
166+
self.viewCache.withLockedValue {
167+
$0[template] = view
134168
}
135-
136-
res.writeHead(200)
137-
res.write(s)
138-
res.end()
139169
}
170+
171+
self.render(path: path, options: viewOptions, to: res)
140172
}
141173
}
142174

Sources/express/View.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ extension Express {
6767
/// The resolved path (for cached views)
6868
var path : String?
6969

70-
public init(name: String, options: Express) {
70+
public required init(name: String, options: Express) {
7171
self.name = name
7272
self.ext = Macro.path.extname(name)
7373
self.engines = options.engines

0 commit comments

Comments
 (0)