Skip to content

Commit 341be35

Browse files
authored
Add browser support (#48)
* feat(test): add browser test system * test(base): add suite on browser tests * refactor: rename browser tests * test(cache): add suite on browser tests * test(clear): add suite on browser tests * test(stale): add suite on browser tests * test(storage-base): add suite on browser tests * test(storage-memory): add suite on browser tests * test(ttl): add suite on browser tests * chore(storage): throw when runs in browser and redis * chore(memory): prevent undefined unref method * feat: add firefox support * feat: add safari support * refactor(abstract-logging): remove dependency and create factory * feat: add rollup bundler support * feat: add webpack bundler support * feat: add browserify bundler support * fix(bench): change createStorage path * chore(package): rename test browser script * feat(ci): add browser ci * chore(readme): add browser documentation * chore: reuse sleep helper method * perf(ci): cache playwright deps * perf(bundlers): dont include redis on browser bundles * feat(bundler): add vite support * chore(test): skip timer tests * chore(test): remove skipped tests * chore(readme): update browser specs * chore(util): comment abstractLogging method * chore(storage): remove istanbul and add test * chore(test-browser): remove duplicated tests * test(storage): add more specific tests * refactor: remove prepare command * ci(browser): remove prepare command
1 parent f08b148 commit 341be35

24 files changed

+756
-12
lines changed

.github/workflows/browsers-ci.yml

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: browsers ci
2+
3+
on:
4+
push:
5+
paths-ignore:
6+
- 'docs/**'
7+
- '*.md'
8+
pull_request:
9+
paths-ignore:
10+
- 'docs/**'
11+
- '*.md'
12+
13+
jobs:
14+
build:
15+
runs-on: ${{ matrix.os }}
16+
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
21+
browser: ['chrome', 'firefox', 'safari', 'edge']
22+
bundler: ['browserify', 'esbuild', 'rollup', 'vite', 'webpack']
23+
exclude:
24+
- os: ubuntu-latest
25+
browser: safari
26+
- os: windows-latest
27+
browser: safari
28+
steps:
29+
- uses: actions/checkout@v3
30+
31+
- name: Use Node.js
32+
uses: actions/setup-node@v3
33+
with:
34+
node-version: 18
35+
36+
- name: Restore cached playwright dependency
37+
uses: actions/cache@v3
38+
id: playwright-cache
39+
with:
40+
path: | # playwright installs browsers into .local-browsers
41+
~/.cache/ms-playwright
42+
~/Library/Caches/ms-playwright
43+
%LOCALAPPDATA%\ms-playwright
44+
key: ${{ matrix.os }}-playwright-${{ hashFiles('package.json') }}
45+
46+
- name: Restore cached dependencies
47+
uses: actions/cache@v3
48+
with:
49+
path: node_modules
50+
key: node-modules-${{ matrix.os }}-${{ hashFiles('package.json') }}
51+
52+
- name: Install dependencies
53+
run: npm install
54+
55+
- name: Install browser
56+
run: ./node_modules/.bin/playwright install ${{ fromJSON('{"chrome":"chromium","edge":"msedge","firefox":"firefox","safari":"webkit"}')[matrix.browser] }}
57+
58+
- name: Run Tests on Browsers
59+
run: npm run test:browser ${{ matrix.browser }} ${{ matrix.bundler }}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,6 @@ dist
108108
.vscode
109109
docker-compose.yml
110110
dump.rdb
111+
112+
# Temporary files used for browser testing
113+
tmp/

README.md

+34
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,40 @@ const p1 = cache.fetchSomething(42) // <--- TypeScript doesn't argue anymore her
311311

312312
---
313313

314+
## Browser
315+
316+
All the major browser are supported; only `memory` storage type is supported, `redis` storage can't be used in a browser env.
317+
318+
This is a very simple example of how to use this module in a browser environment:
319+
320+
```html
321+
<script src="https://unpkg.com/async-cache-dedupe"></script>
322+
323+
<script>
324+
const cache = asyncCacheDedupe.createCache({
325+
ttl: 5, // seconds
326+
storage: { type: 'memory' },
327+
})
328+
329+
cache.define('fetchSomething', async (k) => {
330+
console.log('query', k)
331+
return { k }
332+
})
333+
334+
const p1 = cache.fetchSomething(42)
335+
const p2 = cache.fetchSomething(42)
336+
const p3 = cache.fetchSomething(42)
337+
338+
Promise.all([p1, p2, p3]).then((values) => {
339+
console.log(values)
340+
})
341+
</script>
342+
```
343+
344+
You can also use the module with a bundler. The supported bundlers are `webpack`, `rollup`, `esbuild` and `browserify`.
345+
346+
---
347+
314348
## Maintainers
315349

316350
* [__Matteo Collina__](https://github.com/mcollina), <https://twitter.com/matteocollina>, <https://www.npmjs.com/~matteo.collina>

bench/storage.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { hrtime } = require('process')
44
const path = require('path')
55
const Redis = require('ioredis')
6-
const createStorage = require(path.resolve(__dirname, '../storage'))
6+
const createStorage = require(path.resolve(__dirname, '../src/storage/index.js'))
77

88
// NOTE: this is a very basic benchmarks for tweaking
99
// performance is effected by keys and references size

package.json

+22-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"types": "index.d.ts",
77
"scripts": {
88
"test": "standard | snazzy && tap test/*test.js && tsd",
9+
"test:browser": "node test/browser/helpers/runner-browser.mjs",
910
"lint:fix": "standard --fix",
1011
"redis": "docker run --rm -p 6379:6379 redis redis-server"
1112
},
@@ -30,15 +31,34 @@
3031
"license": "MIT",
3132
"devDependencies": {
3233
"@fastify/pre-commit": "^2.0.2",
34+
"@rollup/plugin-commonjs": "^24.0.1",
35+
"@rollup/plugin-inject": "^5.0.3",
36+
"@rollup/plugin-node-resolve": "^15.0.1",
37+
"browserify": "^17.0.0",
38+
"buffer": "^6.0.3",
39+
"esbuild": "^0.17.4",
40+
"esbuild-plugin-alias": "^0.2.1",
41+
"events": "^3.3.0",
3342
"ioredis": "^5.2.3",
43+
"path-browserify": "^1.0.1",
44+
"playwright": "^1.29.2",
45+
"process": "^0.11.10",
3446
"proxyquire": "^2.1.3",
47+
"rollup": "^3.11.0",
48+
"rollup-plugin-polyfill-node": "^0.12.0",
3549
"snazzy": "^9.0.0",
3650
"standard": "^17.0.0",
51+
"stream-browserify": "^3.0.0",
3752
"tap": "^16.3.0",
38-
"tsd": "^0.25.0"
53+
"tap-mocha-reporter": "^5.0.3",
54+
"tap-parser": "^12.0.1",
55+
"tape": "^5.6.3",
56+
"tsd": "^0.25.0",
57+
"vite": "^4.0.4",
58+
"webpack": "^5.75.0",
59+
"webpack-cli": "^5.0.1"
3960
},
4061
"dependencies": {
41-
"abstract-logging": "^2.0.1",
4262
"mnemonist": "^0.39.2",
4363
"safe-stable-stringify": "^2.3.1"
4464
},

src/storage/index.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
'use strict'
22

3-
const StorageRedis = require('./redis')
3+
const { isServerSide } = require('../util')
4+
5+
let StorageRedis
6+
if (isServerSide) {
7+
StorageRedis = require('./redis')
8+
}
49
const StorageMemory = require('./memory')
510

611
/**
@@ -27,6 +32,10 @@ const StorageOptionsType = {
2732
* @returns {StorageMemory|StorageRedis}
2833
*/
2934
function createStorage (type, options) {
35+
if (!isServerSide && type === StorageOptionsType.redis) {
36+
throw new Error('Redis storage is not supported in the browser')
37+
}
38+
3039
if (type === StorageOptionsType.redis) {
3140
return new StorageRedis(options)
3241
}

src/storage/memory.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict'
22

33
const LRUCache = require('mnemonist/lru-cache')
4-
const nullLogger = require('abstract-logging')
4+
const { abstractLogging } = require('../util')
55
const StorageInterface = require('./interface')
66
const { findMatchingIndexes, findNotMatching, bsearchIndex, wildcardMatch } = require('../util')
77

@@ -26,7 +26,7 @@ class StorageMemory extends StorageInterface {
2626

2727
super(options)
2828
this.size = options.size || DEFAULT_CACHE_SIZE
29-
this.log = options.log || nullLogger
29+
this.log = options.log || abstractLogging()
3030
this.invalidation = options.invalidation || false
3131

3232
this.init()
@@ -401,7 +401,9 @@ function now () {
401401
return _timer
402402
}
403403
_timer = Math.floor(Date.now() / 1000)
404-
setTimeout(_clearTimer, 1000).unref()
404+
const timeout = setTimeout(_clearTimer, 1000)
405+
// istanbul ignore next
406+
if (typeof timeout.unref === 'function') timeout.unref()
405407
return _timer
406408
}
407409

src/storage/redis.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
'use strict'
22

33
const stringify = require('safe-stable-stringify')
4-
const nullLogger = require('abstract-logging')
54
const StorageInterface = require('./interface')
6-
const { findNotMatching, randomSubset } = require('../util')
5+
const { findNotMatching, randomSubset, abstractLogging } = require('../util')
76

87
const GC_DEFAULT_CHUNK = 64
98
const GC_DEFAULT_LAZY_CHUNK = 64
@@ -33,7 +32,7 @@ class StorageRedis extends StorageInterface {
3332
throw new Error('invalidation.referencesTTL must be a positive integer greater than 1')
3433
}
3534

36-
this.log = options.log || nullLogger
35+
this.log = options.log || abstractLogging()
3736
this.store = options.client
3837
this.invalidation = !!options.invalidation
3938
this.referencesTTL = (options.invalidation && options.invalidation.referencesTTL) || REFERENCES_DEFAULT_TTL

src/util.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,28 @@ function wildcardMatch (value, content) {
127127
return i >= value.length - 1
128128
}
129129

130+
// `abstract-logging` dependency has been removed because there is a bug on Rollup
131+
// https://github.com/jsumners/abstract-logging/issues/6
132+
function abstractLogging () {
133+
const noop = () => {}
134+
return {
135+
fatal: noop,
136+
error: noop,
137+
warn: noop,
138+
info: noop,
139+
debug: noop,
140+
trace: noop
141+
}
142+
}
143+
144+
const isServerSide = typeof window === 'undefined'
145+
130146
module.exports = {
131147
findNotMatching,
132148
findMatchingIndexes,
133149
bsearchIndex,
134150
wildcardMatch,
135-
136-
randomSubset
151+
randomSubset,
152+
abstractLogging,
153+
isServerSide
137154
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as processModule from 'process'
2+
3+
export const process = processModule
4+
5+
export function setImmediate (fn, ...args) {
6+
setTimeout(() => fn(...args), 0)
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { build } from 'esbuild'
2+
import alias from 'esbuild-plugin-alias'
3+
import { createRequire } from 'module'
4+
5+
const require = createRequire(import.meta.url)
6+
7+
build({
8+
entryPoints: ['test/browser/test-browser.js'],
9+
outfile: 'tmp/esbuild/suite.browser.js',
10+
bundle: true,
11+
platform: 'browser',
12+
plugins: [
13+
alias({
14+
path: require.resolve('path-browserify'),
15+
stream: require.resolve('stream-browserify')
16+
})
17+
],
18+
define: {
19+
global: 'globalThis'
20+
},
21+
inject: ['test/browser/fixtures/esbuild.browser-shims.mjs'],
22+
external: ['./src/storage/redis.js']
23+
}).catch((err) => {
24+
console.log(err)
25+
process.exit(1)
26+
})

test/browser/fixtures/index.html

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<style>
5+
body {
6+
margin: 0;
7+
padding: 0;
8+
}
9+
10+
#output {
11+
margin: 30px;
12+
white-space: pre;
13+
font-family: monospace;
14+
}
15+
16+
.error {
17+
display: block;
18+
width: calc(100% - 20px);
19+
background-color: #bb0000;
20+
padding: 10px;
21+
border-radius: 5px;
22+
margin: 10px 0;
23+
color: white;
24+
}
25+
</style>
26+
</head>
27+
<body>
28+
<div id="output"></div>
29+
30+
<script type="text/javascript">
31+
let failed = false
32+
const output = document.querySelector('#output')
33+
const originalLog = console.log
34+
const originalError = console.error
35+
36+
console.log = function (message, ...args) {
37+
if (typeof message !== 'string') {
38+
originalLog(message, ...args)
39+
return
40+
}
41+
42+
if (message.includes('not ok')) {
43+
failed = true
44+
document.body.style.backgroundColor = '#ff9d9d'
45+
} else if (message.includes('# async-cache-dedupe-finished') && !failed) {
46+
document.body.style.backgroundColor = '#9dff9d'
47+
}
48+
49+
const span = document.createElement('span')
50+
span.textContent = message + '\n'
51+
output.appendChild(span)
52+
window.scrollTo(0, document.body.scrollHeight)
53+
originalLog(message, ...args)
54+
}
55+
56+
console.error = function (message, ...args) {
57+
if (typeof message !== 'string') {
58+
originalError(message, ...args)
59+
return
60+
}
61+
62+
const span = document.createElement('span')
63+
span.classList.add('error')
64+
span.textContent = message + '\n'
65+
output.appendChild(span)
66+
67+
originalError(message, ...args)
68+
}
69+
</script>
70+
<script type="text/javascript" src="./suite.browser.js"></script>
71+
</body>
72+
</html>

0 commit comments

Comments
 (0)