-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
248 lines (233 loc) · 7.74 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//. # Booture
//.
//. Application bootstrapping on top of [Fluture Hooks][].
//.
//. Booture uses Hooks (as you might expect) to ensure that whatever happens,
//. once a service is acquired, it will always be disposed. Furthermore,
//. acquisition and disposal of services happens at optimal parallelism.
//.
//. Booture exposes a single function: [`bootstrap`](#bootstrap), which in
//. combination with [Fluture][] and [Fluture Hooks][], provides an ideal
//. platform for control over your application lifecycle.
//.
//. ## Usage
//.
//. ### Node
//.
//. ```console
//. $ npm install --save fluture booture fluture-hooks
//. ```
//.
//. On Node 12 and up, this module can be loaded directly with `import` or
//. `require`. On Node versions below 12, `require` or the [esm][]-loader can
//. be used.
//.
//. ### Deno and Modern Browsers
//.
//. You can load the EcmaScript module from various content delivery networks:
//.
//. - [Skypack](https://cdn.skypack.dev/[email protected])
//. - [JSPM](https://jspm.dev/[email protected])
//. - [jsDelivr](https://cdn.jsdelivr.net/npm/[email protected]/+esm)
//.
//. ### Old Browsers and Code Pens
//.
//. There's a [UMD][] file included in the NPM package, also available via
//. jsDelivr: https://cdn.jsdelivr.net/npm/[email protected]/dist/umd.js
//.
//. This file adds `booture` to the global scope, or use CommonJS/AMD
//. when available.
//.
//. ### Usage Example
//.
//. The example below defines four "services": `config`, `postgres`, `redis`,
//. and `app`. The App depends on Redis and Postgres having been initialized,
//. which in turn depend on the Config service.
//.
//. The consumption of these services happens in the form of binding the App to
//. a port, and waiting for SIGINT to complete the consumption.
//.
//. ```js
//. import {Future, node, fork, attempt} from 'fluture';
//. import {bootstrap} from 'booture';
//. import {hook, acquire, runHook} from 'fluture-hooks';
//.
//. const acquireConfig = (
//. attempt (() => ({
//. redis: {url: process.env.REDIS_URL},
//. postgres: {url: process.env.POSTGRES_URL},
//. }))
//. );
//.
//. const acquirePostgres = config => (
//. node (done => require ('imaginary-postgres').connect (config, done))
//. );
//.
//. const acquireRedis = config => (
//. node (done => require ('imaginary-redis').connect (config, done))
//. );
//.
//. const closeConnection = connection => (
//. node (done => connection.end (done))
//. );
//.
//. const acquireApp = (redis, postgres) => (
//. attempt (() => require ('./imaginary-app').create (redis, postgres))
//. );
//.
//. const bootstrapConfig = {
//. name: 'config',
//. needs: [],
//. bootstrap: () => acquire (acquireConfig),
//. };
//.
//. const bootstrapPostgres = {
//. name: 'postgres',
//. needs: ['config'],
//. bootstrap: ({config}) => hook (acquirePostgres (config.postgres))
//. (closeConnection),
//. };
//.
//. const bootstrapRedis = {
//. name: 'redis',
//. needs: ['config'],
//. bootstrap: ({config}) => hook (acquireRedis (config.redis))
//. (closeConnection),
//. };
//.
//. const bootstrapApp = {
//. name: 'app',
//. needs: ['redis, postgres'],
//. bootstrap: ({redis, postgres}) => acquire (acquireApp (redis, postgres)),
//. };
//.
//. const servicesHook = bootstrap ([ bootstrapConfig,
//. bootstrapPostgres,
//. bootstrapRedis,
//. bootstrapApp ]);
//.
//. const withServices = runHook (servicesHook);
//.
//. const program = withServices (({app}) => Future ((rej, res) => {
//. const conn = app.listen (3000);
//. conn.once ('error', rej);
//. process.once ('SIGINT', res);
//. }));
//.
//. fork (console.error) (console.log) (program);
//. ```
//.
//. Some things to note about the example above, and general usage of Booture:
//.
//. 1. `servicesHook` is a `Hook`, so before running it, it can be composed
//. with other hooks using `map`, `ap`, and `chain`, and even used in the
//. definition of other bootstrappers.
//. 2. `program` is a `Future`, so nothing happens until it's forked. Before
//. forking it, it can be composed with other Futures using `map`, `ap`,
//. `bimap`, and `chain`, or any of the other functions provided by Fluture.
import {map, chain} from 'fluture/index.js';
import {hookAll, Hook} from 'fluture-hooks/index.js';
// hasProp :: Object ~> String -> Boolean
const hasProp = Object.prototype.hasOwnProperty;
const check = bootstrappers => {
const indexed = bootstrappers.reduce ((o, boot) => Object.assign ({}, o, {
[boot.name]: (o[boot.name] || []).concat ([boot]),
}), {});
const doubles = Object.values (indexed).filter (({length}) => length > 1);
if (doubles.length > 0) {
throw new TypeError (`Flawed dependency graph:\n${
doubles.map (boots => (
` - [${boots[0].name}] has ${boots.length} providers:\n${
boots.map (boot => (
` - One depending on [${boot.needs.join ('; ')}]`
)).join ('; and\n')
}`
)).join ('\n')
}`);
}
const validateTree = ({path, checked, flaws}, {name, needs}) => {
if (checked.includes (name)) {
return {path, flaws, checked};
}
if (path.includes (name)) {
return {
path: path,
checked: checked.concat ([name]),
flaws: flaws.concat ([
`[${
name
}] circles around via [${
path.slice (path.indexOf (name)).join (' -> ')
} -> ${
name
}]`,
]),
};
}
const subs = needs.reduce ((acc, need) => (
indexed[need] ?
validateTree (acc, indexed[need][0]) :
{
path: acc.path,
checked: acc.checked.concat ([need]),
flaws: acc.flaws.concat ([
`[${name}] needs [${need}], which has no provider`,
]),
}
), {path: path.concat ([name]), checked, flaws});
return {path, checked: subs.checked.concat ([name]), flaws: subs.flaws};
};
const {flaws} = bootstrappers.reduce (
validateTree,
{path: [], flaws: [], checked: []}
);
if (flaws.length > 0) {
throw new TypeError (`Flawed dependency graph:\n${
flaws.map (flaw => ` - ${flaw}`).join ('\n')
}`);
}
};
const callBootstrappers = (bootstrappers, resources) => (
map (xs => xs.reduce ((acc, {resource, name}) => (
Object.assign ({}, acc, {[name]: resource})
), resources)) (hookAll (bootstrappers.map (({name, bootstrap}) => (
map (resource => ({resource, name})) (bootstrap (resources))
))))
);
const complete = (bootstrappers, hookResources) => (
bootstrappers.length === 0 ? hookResources : chain (resources => {
const pred = ({needs}) => needs.every (k => hasProp.call (resources, k));
const layer = bootstrappers.filter (pred);
const remainder = bootstrappers.filter (x => !pred (x));
return complete (remainder, callBootstrappers (layer, resources));
}) (hookResources)
);
//. ## API
//.
//. ### Types
//.
//. ```hs
//. type Name = String
//. type Services a = Dict Name a
//. data Bootstrapper a b = Bootstrapper {
//. name :: Name,
//. needs :: Array Name,
//. bootstrap :: Services b -> Hook (Future c a) b
//. }
//. ```
//.
//. ### Functions
//.
//# bootstrap :: Array (Bootstrapper a b) -> Hook (Future c a) (Services b)
//.
//. Given a list of service bootstrappers, returns a `Hook` that represents the
//. acquisition and disposal of these services. Running the hook allows for
//. consumption of the services.
export const bootstrap = bootstrappers => {
check (bootstrappers);
return complete (bootstrappers, Hook.of ({}));
};
//. [Fluture]: https://github.com/fluture-js/fluture
//. [Fluture Hooks]: https://github.com/fluture-js/fluture-hooks
//. [esm]: https://github.com/standard-things/esm
//. [UMD]: https://github.com/umdjs/umd