Skip to content

Commit

Permalink
chore: working on chapter 5
Browse files Browse the repository at this point in the history
  • Loading branch information
lmammino committed Oct 21, 2024
1 parent eab0029 commit 3fa3cad
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 01-delay-with-promises

This sample demonstrates how to use the `Promise` constructor to create a new `Promise` from scratch.

## Run

To run the example launch:

```bash
node index.js
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
function delay(milliseconds) {
return new Promise((resolve, _reject) => {
setTimeout(() => {
resolve(Date.now())
}, milliseconds)
})
}

console.log(`Delaying... (${Date.now()})`)

delay(1000).then(newDate => {
console.log(`Done (${newDate})`)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "01-delay-with-promises",
"version": "1.0.0",
"description": "This sample demonstrates how to use the `Promise` constructor to create a new `Promise` from scratch",
"type": "module",
"scripts": {},
"engines": {
"node": ">=22"
},
"engineStrict": true,
"keywords": [],
"author": "Luciano Mammino and Mario Casciaro",
"license": "MIT",
"dependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 02-promisify

This sample demonstrates how to *promisify* a Node.js-style callback-based function.

## Run

To run the example launch:

```bash
node index.js
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { randomBytes } from 'node:crypto'

function promisify(callbackBasedFn) {
return function promisifiedFn(...args) {
return new Promise((resolve, reject) => {
const newArgs = [
...args,
(err, result) => {
if (err) {
return reject(err)
}

resolve(result)
},
]
callbackBasedFn(...newArgs)
})
}
}

const randomBytesP = promisify(randomBytes)
randomBytesP(32).then(buffer => {
console.log(`Random bytes: ${buffer.toString()}`)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "02-promisify",
"version": "1.0.0",
"description": "This sample demonstrates how to *promisify* a Node.js-style callback-based function",
"type": "module",
"scripts": {},
"engines": {
"node": ">=22"
},
"engineStrict": true,
"keywords": [],
"author": "Luciano Mammino and Mario Casciaro",
"license": "MIT",
"dependencies": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 03-wpromises-eb-spider-v2

Web spider example to demostrate sequential asynchronous execution with Promises

## Run

Install the necessary dependencies with `npm install` and then run:

```bash
node spider-cli.js https://loige.co
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "03-wpromises-eb-spider-v2",
"version": "1.0.0",
"description": "Web spider example to demostrate sequential asynchronous execution with Promises",
"type": "module",
"scripts": {},
"engines": {
"node": ">=22"
},
"engineStrict": true,
"keywords": [],
"author": "Luciano Mammino and Mario Casciaro",
"license": "MIT",
"dependencies": {
"htmlparser2": "^9.1.0",
"mkdirp": "^3.0.1",
"slug": "^9.1.0"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { spider } from './spider.js'

const url = process.argv[2]
const maxDepth = Number.parseInt(process.argv[3], 10) || 1

spider(url, maxDepth)
.then(() => console.log('Downloaded complete'))
.catch(err => {
console.error(err)
process.exit(1)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { readFile, writeFile } from 'node:fs/promises'
import { dirname } from 'node:path'
import {
exists,
get,
getPageLinks,
recursiveMkdir,
urlToFilename,
} from './utils.js'

function saveFile(filename, content) {
return recursiveMkdir(dirname(filename))
.then(() => writeFile(filename, content))
.then(() => content)
}

function download(url, filename) {
console.log(`Downloading ${url} into ${filename}`)
return get(url).then(content => saveFile(filename, content))
}

function spiderLinks(currentUrl, body, maxDepth) {
let promise = Promise.resolve()
if (maxDepth === 0) {
return promise
}

const links = getPageLinks(currentUrl, body)
for (const link of links) {
promise = promise.then(() => spider(link, maxDepth - 1))
}

return promise
}

export function spider(url, maxDepth) {
const filename = urlToFilename(url)

return exists(filename).then(alreadyExists => {
if (alreadyExists) {
if (!filename.endsWith('.html')) {
// ignoring non-HTML resources
return
}

return readFile(filename, 'utf8').then(fileContent =>
spiderLinks(url, fileContent, maxDepth)
)
}

// if file does not exist, download it
return download(url, filename).then(fileContent => {
// if the file is an HTML file, spider it
if (filename.endsWith('.html')) {
return spiderLinks(url, fileContent.toString('utf8'), maxDepth)
}
// otherwise, stop here
return
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { constants } from 'node:fs'
import { access } from 'node:fs/promises'
import { extname, join } from 'node:path'
import { Parser } from 'htmlparser2'
import { mkdirp } from 'mkdirp'
import slug from 'slug'

export function exists(filePath) {
return access(filePath, constants.F_OK)
.then(() => true)
.catch(err => {
if (err.code === 'ENOENT') {
return false
}

throw err
})
}

export function urlToFilename(url) {
const parsedUrl = new URL(url)
const urlComponents = parsedUrl.pathname.split('/')
const originalFileName = urlComponents.pop()
const urlPath = urlComponents
.filter(component => component !== '')
.map(component => slug(component, { remove: null }))
.join('/')
const basePath = join(parsedUrl.hostname, urlPath)
const missingExtension = !originalFileName || extname(originalFileName) === ''
if (missingExtension) {
return join(basePath, originalFileName, 'index.html')
}

return join(basePath, originalFileName)
}

// NOTE: this function is just for illustrative purposes. We are wrapping
// fetch in a simplified API because at this point of the book we want
// to demonstrate some promise based patterns
export function get(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`)
}
// NOTE: this loads all the content in memory and therefore is not suitable
// to handle large payloads.
// For large payloads, we would need to use a stream-based approach
return response.arrayBuffer()
})
.then(content => Buffer.from(content))
}

// NOTE: this function is just for illustrative purposes. We are aliasing
// `mkdirp` just to keep the same naming conventions as the ones we had in the callback-based
// version of this example
export const recursiveMkdir = mkdirp

export function getPageLinks(currentUrl, body) {
const url = new URL(currentUrl)
const internalLinks = []
const parser = new Parser({
onopentag(name, attribs) {
if (name === 'a' && attribs.href) {
const newUrl = new URL(attribs.href, url)
if (
newUrl.hostname === url.hostname &&
newUrl.pathname !== url.pathname
) {
internalLinks.push(newUrl.toString())
}
}
},
})
parser.end(body)

return internalLinks
}

0 comments on commit 3fa3cad

Please sign in to comment.