1 |
|
2 | import './Globals'
|
3 |
|
4 | const Bluebird = require('bluebird')
|
5 | Promise = Bluebird
|
6 |
|
7 |
|
8 | import * as Log from './log'
|
9 | import {TypeStoreModelKey} from './Constants'
|
10 | import {
|
11 | IStorePlugin,
|
12 | ICoordinatorOptions,
|
13 | IModel,
|
14 | IModelType,
|
15 | CoordinatorOptions,
|
16 | ICoordinator,
|
17 | IPlugin, PluginType
|
18 | } from './Types'
|
19 | import {msg, Strings} from "./Messages"
|
20 | import {Repo} from "./Repo";
|
21 | import {PluginFilter, assert, PromiseMap, isNil} from "./Util";
|
22 | import {IModelOptions} from "./decorations/ModelDecorations";
|
23 | import {PluginEventType} from "./PluginTypes";
|
24 |
|
25 |
|
26 | const log = Log.create(__filename)
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export type TModelTypeMap = {[clazzName:string]:IModelType}
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | export class Coordinator implements ICoordinator {
|
37 |
|
38 | private plugins:IPlugin[] = []
|
39 |
|
40 | notify(eventType:PluginEventType,...args:any[]) {
|
41 | this.plugins.forEach(plugin => plugin.handle(eventType,...args))
|
42 | }
|
43 |
|
44 | |
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | |
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | private modelMap:TModelTypeMap = {}
|
60 | private models:IModelType[] = []
|
61 |
|
62 |
|
63 | |
64 |
|
65 |
|
66 |
|
67 |
|
68 | getModels():IModelType[] {
|
69 | return this.models
|
70 | }
|
71 |
|
72 | private findModel(predicate) {
|
73 | for (let modelType of this.models) {
|
74 | if (predicate(modelType)) {
|
75 | return modelType
|
76 | }
|
77 | }
|
78 |
|
79 | log.debug('unable to find registered model for clazz in',Object.keys(this.modelMap))
|
80 | return null
|
81 | }
|
82 |
|
83 | getModel(clazz:any):IModelType {
|
84 | return this.findModel((model) => model.clazz === clazz)
|
85 | }
|
86 |
|
87 | getModelByName(name:string):IModelType {
|
88 | return this.findModel((model) => model.name === name)
|
89 | }
|
90 |
|
91 | |
92 |
|
93 |
|
94 | private options:ICoordinatorOptions = new CoordinatorOptions(null)
|
95 |
|
96 | getOptions() {
|
97 | return this.options
|
98 | }
|
99 |
|
100 |
|
101 | private initialized = false
|
102 |
|
103 |
|
104 |
|
105 | private startPromise:Promise<any> = null
|
106 | private internal = {
|
107 | started:false
|
108 | }
|
109 | get started() {
|
110 | return this.startPromise !== null && this.internal.started
|
111 | }
|
112 |
|
113 | set started(newVal:boolean) {
|
114 | this.internal.started = newVal
|
115 | }
|
116 |
|
117 |
|
118 |
|
119 | private checkInitialized(not:boolean = false) {
|
120 | this.checkStarted(true)
|
121 | assert(not ? !this.initialized : this.initialized,
|
122 | msg(not ? Strings.ManagerInitialized : Strings.ManagerNotInitialized))
|
123 | }
|
124 |
|
125 |
|
126 |
|
127 | private checkStarted(not:boolean = false) {
|
128 | const valid = (not) ? !this.started : this.started
|
129 |
|
130 | assert(valid, msg(not ? Strings.ManagerSettled : Strings.ManagerNotSettled))
|
131 | }
|
132 |
|
133 | stores() {
|
134 | return PluginFilter<IStorePlugin>(this.plugins,PluginType.Store)
|
135 | }
|
136 |
|
137 |
|
138 |
|
139 | |
140 |
|
141 |
|
142 | init(newOptions:ICoordinatorOptions, ...newPlugins:IPlugin[]):Promise<ICoordinator> {
|
143 | this.checkStarted(true)
|
144 | this.checkInitialized(true)
|
145 | this.initialized = true
|
146 | this.plugins.push(...newPlugins)
|
147 |
|
148 |
|
149 | this.options = this.options || newOptions
|
150 | Object.assign(this.options,newOptions)
|
151 |
|
152 |
|
153 |
|
154 | assert(this.stores().length > 0,msg(Strings.ManagerTypeStoreRequired))
|
155 |
|
156 |
|
157 | log.debug(msg(Strings.ManagerInitComplete))
|
158 | return Bluebird.all(
|
159 | this.plugins
|
160 | .filter(plugin => !isNil(plugin))
|
161 | .map(plugin => plugin.init(this,this.options))
|
162 | ).return(this)
|
163 |
|
164 | }
|
165 |
|
166 |
|
167 |
|
168 | |
169 |
|
170 |
|
171 |
|
172 |
|
173 | start(...models):Promise<ICoordinator> {
|
174 | this.checkStarted(true)
|
175 | models.forEach(model => this.registerModel(model))
|
176 |
|
177 | this.startPromise = PromiseMap(this.plugins, plugin => (plugin) && plugin.start())
|
178 |
|
179 | return Bluebird.resolve(this.startPromise)
|
180 | .return(this)
|
181 | .catch(err => {
|
182 | log.error('failed to start coordinator',err)
|
183 | throw err
|
184 | })
|
185 | .finally(() => {
|
186 | this.started = true
|
187 | this.startPromise = null
|
188 | })
|
189 |
|
190 |
|
191 | }
|
192 |
|
193 | async stop():Promise<ICoordinator> {
|
194 | if (!this.started)
|
195 | return this
|
196 |
|
197 | try {
|
198 | await (this.startPromise) ?
|
199 | this.startPromise.then(this.stopPlugins.bind(this)) :
|
200 | this.stopPlugins()
|
201 | } catch (err) {
|
202 | log.error(`Coordinator shutdown was not clean`)
|
203 | } finally {
|
204 | this.startPromise = null
|
205 | this.started = false
|
206 | this.initialized = false
|
207 | this.plugins = []
|
208 | this.models = []
|
209 | this.modelMap = {}
|
210 | }
|
211 | return this
|
212 | }
|
213 |
|
214 |
|
215 | |
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 | async execute<T>(fn:Function):Promise<T> {
|
223 | return new Promise<T>((resolve,reject) => {
|
224 |
|
225 | function executeFn(...args) {
|
226 | const result = fn(...args)
|
227 | resolve(result)
|
228 | }
|
229 |
|
230 | function handleError(err) {
|
231 | const fnName = (fn) ? (fn as any).name : null
|
232 | log.error(msg(Strings.ManagerErrorFn,fnName ? fnName : 'UNKNOWN'), err)
|
233 | reject(err)
|
234 | }
|
235 |
|
236 | return (this.startPromise) ?
|
237 | this.startPromise.then(executeFn).catch(handleError) :
|
238 | Promise.resolve(executeFn).catch(handleError)
|
239 |
|
240 | })
|
241 |
|
242 | }
|
243 |
|
244 | async stopPlugins() {
|
245 | await PromiseMap(this.plugins, plugin => (plugin) && plugin.stop())
|
246 | }
|
247 |
|
248 | /**
|
249 | * Reset the coordinator status
|
250 | *
|
251 | * @returns {Coordinator.reset}
|
252 | */
|
253 | async reset():Promise<ICoordinator> {
|
254 | await this.stop()
|
255 |
|
256 |
|
257 | return this
|
258 |
|
259 | }
|
260 |
|
261 | /**
|
262 | * Register a model with the system
|
263 | *
|
264 | * @param constructor
|
265 | */
|
266 | registerModel(constructor:Function) {
|
267 | this.checkStarted(true)
|
268 |
|
269 | let model = this.getModel(constructor)
|
270 | if (model) {
|
271 | log.debug(`Trying to register ${model.name} a second time? is autoregister enabled?`)
|
272 | return
|
273 | }
|
274 |
|
275 | const modelOpts:IModelOptions = Reflect.getMetadata(TypeStoreModelKey,constructor)
|
276 | if (!modelOpts) {
|
277 | log.info(`Can not register a model without metadata ${(constructor && constructor.name) || 'unknown'}`)
|
278 | return
|
279 | }
|
280 |
|
281 | model = {
|
282 | options: modelOpts,
|
283 | name: modelOpts.clazzName,
|
284 | clazz: constructor
|
285 | }
|
286 |
|
287 | this.modelMap[modelOpts.clazzName] = model
|
288 | this.models.push(model)
|
289 | this.notify(PluginEventType.ModelRegister,model)
|
290 | return this
|
291 | }
|
292 |
|
293 |
|
294 | private repoMap = new WeakMap<any,any>()
|
295 |
|
296 | /**
|
297 | * Get a repository for the specified model/class
|
298 | *
|
299 | * @param clazz
|
300 | * @returns {T}
|
301 | */
|
302 | getRepo<T extends Repo<M>,M extends IModel>(clazz:{new(): T; }):T {
|
303 | let repo:T = this.repoMap.get(clazz)
|
304 | if (repo)
|
305 | return repo
|
306 |
|
307 | repo = new clazz()
|
308 | repo.init(this)
|
309 |
|
310 | this.notify(PluginEventType.RepoInit,repo)
|
311 |
|
312 | repo.start()
|
313 | this.repoMap.set(clazz,repo)
|
314 | return repo
|
315 | }
|
316 |
|
317 | }
|
318 |
|
\ | No newline at end of file |