@ditojs/server 1.5.1 → 1.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "1.5.1",
3
+ "version": "1.5.4",
4
4
  "type": "module",
5
5
  "description": "Dito.js Server – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/server",
@@ -30,7 +30,7 @@
30
30
  "@originjs/vite-plugin-commonjs": "^1.0.3",
31
31
  "ajv": "^8.11.0",
32
32
  "ajv-formats": "^2.1.1",
33
- "aws-sdk": "^2.1106.0",
33
+ "aws-sdk": "^2.1108.0",
34
34
  "axios": "^0.26.1",
35
35
  "bcryptjs": "^2.4.3",
36
36
  "bytes": "^3.1.2",
@@ -76,10 +76,10 @@
76
76
  "objection": "^3.0.1"
77
77
  },
78
78
  "devDependencies": {
79
- "knex": "^1.0.4",
79
+ "knex": "^1.0.5",
80
80
  "objection": "^3.0.1",
81
81
  "pg": "^8.7.3",
82
82
  "sqlite3": "^5.0.2"
83
83
  },
84
- "gitHead": "40731960cf8b528f4f40f8b7e43e2e966138337c"
84
+ "gitHead": "fa4ff0e996e9d043a226d54c5e9ab263ccf1ad5f"
85
85
  }
@@ -22,8 +22,8 @@ import helmet from 'koa-helmet'
22
22
  import responseTime from 'koa-response-time'
23
23
  import Router from '@ditojs/router'
24
24
  import {
25
- isArray, isObject, isString, asArray, isPlainObject, hyphenate, clone, merge,
26
- parseDataPath, normalizeDataPath, isModule
25
+ isArray, isObject, isString, asArray, isPlainObject, isModule,
26
+ hyphenate, clone, merge, parseDataPath, normalizeDataPath, toPromiseCallback
27
27
  } from '@ditojs/utils'
28
28
  import SessionStore from './SessionStore.js'
29
29
  import { Validator } from './Validator.js'
@@ -88,6 +88,9 @@ export class Application extends Koa {
88
88
  this.models = Object.create(null)
89
89
  this.services = Object.create(null)
90
90
  this.controllers = Object.create(null)
91
+ this.server = null
92
+ this.isRunning = false
93
+
91
94
  this.setupLogger()
92
95
  this.setupKnex()
93
96
  this.setupMiddleware(middleware)
@@ -283,10 +286,6 @@ export class Application extends Koa {
283
286
  return Object.values(this.services).find(callback)
284
287
  }
285
288
 
286
- forEachService(callback) {
287
- return Promise.all(Object.values(this.services).map(callback))
288
- }
289
-
290
289
  addControllers(controllers, namespace) {
291
290
  for (const [key, value] of Object.entries(controllers)) {
292
291
  if (isModule(value) || isPlainObject(value)) {
@@ -718,46 +717,56 @@ export class Application extends Koa {
718
717
  this.on('error', this.logError)
719
718
  }
720
719
  await this.emit('before:start')
721
- await this.forEachService(service => service.start())
722
- const { server: { host, port } } = this.config
723
- this.server = await new Promise((resolve, reject) => {
724
- const server = this.listen(port, host, () => {
725
- const { port } = server.address()
720
+ this.server = await new Promise(resolve => {
721
+ const server = this.listen(this.config.server, () => {
722
+ const { address, port } = server.address()
726
723
  console.info(
727
- `Dito server started at http://${host}:${port}`
724
+ `Dito.js server started at http://${address}:${port}`
728
725
  )
729
726
  resolve(server)
730
727
  })
731
- if (!server) {
732
- reject(new Error(`Unable to start server at http://${host}:${port}`))
733
- }
734
728
  })
729
+ if (!this.server) {
730
+ throw new Error('Unable to start Dito.js server')
731
+ }
732
+ this.isRunning = true
735
733
  await this.emit('after:start')
736
734
  }
737
735
 
738
- async stop() {
739
- await this.emit('before:stop')
740
- this.server = await new Promise((resolve, reject) => {
741
- const { server } = this
742
- if (server) {
743
- server.close(err => {
744
- if (err) {
745
- reject(err)
746
- } else {
747
- resolve(null)
748
- }
749
- })
750
- // Hack to make sure that we close the server,
751
- // even if sockets are still open.
752
- // Taken from https://stackoverflow.com/a/36830072.
753
- // A proper solution would be to use a library, ex: https://github.com/godaddy/terminus
754
- setImmediate(() => server.emit('close'))
755
- } else {
756
- reject(new Error('Server is not running'))
757
- }
758
- })
759
- await this.forEachService(service => service.stop())
760
- await this.emit('after:stop')
736
+ async stop(timeout = 0) {
737
+ if (!this.server) {
738
+ throw new Error('Dito.js server is not running')
739
+ }
740
+
741
+ const promise = (async () => {
742
+ await this.emit('before:stop')
743
+ this.isRunning = false
744
+ await new Promise((resolve, reject) => {
745
+ this.server.close(toPromiseCallback(resolve, reject))
746
+ })
747
+ // Hack to make sure that the server is closed, even if sockets are still
748
+ // open after `server.close()`, see: https://stackoverflow.com/a/36830072
749
+ this.server.emit('close')
750
+ this.server = null
751
+ await this.emit('after:stop')
752
+ })()
753
+
754
+ if (timeout > 0) {
755
+ await Promise.race([
756
+ promise,
757
+ new Promise((resolve, reject) =>
758
+ setTimeout(reject,
759
+ timeout,
760
+ new Error(
761
+ `Timeout reached while stopping Dito.js server (${timeout}ms)`
762
+ )
763
+ )
764
+ )
765
+ ])
766
+ } else {
767
+ await promise
768
+ }
769
+
761
770
  if (this.config.log.errors !== false) {
762
771
  this.off('error', this.logError)
763
772
  }
@@ -1,4 +1,5 @@
1
1
  import path from 'path'
2
+ import { exit } from 'process'
2
3
  import Koa from 'koa'
3
4
  import serve from 'koa-static'
4
5
  import { defineConfig, createServer } from 'vite'
@@ -32,6 +33,7 @@ export class AdminController extends Controller {
32
33
  this.mode = this.config.mode || (
33
34
  this.app.config.env === 'development' ? 'development' : 'production'
34
35
  )
36
+ this.closed = false
35
37
  }
36
38
 
37
39
  getPath(name) {
@@ -80,16 +82,25 @@ export class AdminController extends Controller {
80
82
  // Shield admin views against unauthorized access.
81
83
  const authorization = this.processAuthorize(this.authorize)
82
84
  return async (ctx, next) => {
83
- if (ctx.url === '/dito.js') {
84
- // Return without calling `next()`
85
- return this.sendDitoObject(ctx)
86
- } else if (/\/views\b/.test(ctx.url)) {
87
- await this.handleAuthorization(authorization, ctx)
85
+ if (this.closed) {
86
+ // Avoid strange behavior during shut-down of the vite dev server.
87
+ // Sending back a 408 response seems to work best, while a 503 sadly
88
+ // would put the client into a state that prevents the server from a
89
+ // proper shut-down, and a 205 would kill future hot-reloads.
90
+ ctx.status = 408 // Request Timeout
91
+ } else if (ctx.url === '/dito.js') {
92
+ // Don't call `next()`
93
+ this.sendDitoObject(ctx)
94
+ } else {
95
+ if (/\/views\b/.test(ctx.url)) {
96
+ await this.handleAuthorization(authorization, ctx)
97
+ }
98
+ await next()
88
99
  }
89
- await next()
90
100
  }
91
101
  }
92
102
 
103
+ // @override
93
104
  compose() {
94
105
  this.koa = new Koa()
95
106
  this.koa.use(this.middleware())
@@ -128,6 +139,33 @@ export class AdminController extends Controller {
128
139
  }
129
140
  }
130
141
  })
142
+
143
+ this.closed = false
144
+
145
+ // Monkey-patch `process.exit()` to filter out the calls caused by vite's
146
+ // handling of SIGTERM, see: https://github.com/vitejs/vite/issues/7627
147
+ process.exit = code => {
148
+ // Filter out calls from inside vite by looking at the stack trace.
149
+ if (new Error().stack.includes('/vite/dist/')) {
150
+ // vite's own `exitProcess()` just called `process.exit(), and this
151
+ // means it has already called `server.close()` internally.
152
+ this.closed = true
153
+ process.exit = exit
154
+ } else {
155
+ exit(code)
156
+ }
157
+ }
158
+
159
+ this.app.once('after:stop', () => {
160
+ // For good timing it seems crucial to not add more ticks with async
161
+ // signature, so we directly return the `server.close()` promise instead.
162
+ process.exit = exit
163
+ if (!this.closed) {
164
+ this.closed = true
165
+ return server.close()
166
+ }
167
+ })
168
+
131
169
  this.koa.use(handleConnectMiddleware(server.middlewares, {
132
170
  expandMountPath: true
133
171
  }))
@@ -26,6 +26,12 @@ export class Controller {
26
26
  initialize() {
27
27
  }
28
28
 
29
+ // @return {Application|Function} [app or function]
30
+ compose() {
31
+ // To be overridden in sub-classes, if the controller needs to install
32
+ // middleware. For normal routes, use `this.app.addRoute()` instead.
33
+ }
34
+
29
35
  setup(isRoot = true, setupActionsObject = true) {
30
36
  this._setupEmitter(this.hooks, {
31
37
  // Support wildcard hooks only on controllers:
@@ -246,12 +252,6 @@ export class Controller {
246
252
  ])
247
253
  }
248
254
 
249
- // @return {Application|Function} [app or function]
250
- compose() {
251
- // To be overridden in sub-classes, if the controller needs to install
252
- // middleware. For normal routes, use `this.app.addRoute()` instead.
253
- }
254
-
255
255
  getPath(type, path) {
256
256
  // To be overridden by sub-classes.
257
257
  return path
@@ -11,6 +11,8 @@ export class Service {
11
11
 
12
12
  setup(config) {
13
13
  this.config = config
14
+ this.app.on('before:start', () => this.start())
15
+ this.app.on('after:stop', () => this.stop())
14
16
  }
15
17
 
16
18
  // @overridable