Skip to content

Commit

Permalink
Add browser support (#48)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mateonunez committed Feb 10, 2023
1 parent f08b148 commit 341be35
Show file tree
Hide file tree
Showing 24 changed files with 756 additions and 12 deletions.
59 changes: 59 additions & 0 deletions .github/workflows/browsers-ci.yml
@@ -0,0 +1,59 @@
name: browsers ci

on:
push:
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'

jobs:
build:
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
browser: ['chrome', 'firefox', 'safari', 'edge']
bundler: ['browserify', 'esbuild', 'rollup', 'vite', 'webpack']
exclude:
- os: ubuntu-latest
browser: safari
- os: windows-latest
browser: safari
steps:
- uses: actions/checkout@v3

- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 18

- name: Restore cached playwright dependency
uses: actions/cache@v3
id: playwright-cache
with:
path: | # playwright installs browsers into .local-browsers
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
%LOCALAPPDATA%\ms-playwright
key: ${{ matrix.os }}-playwright-${{ hashFiles('package.json') }}

- name: Restore cached dependencies
uses: actions/cache@v3
with:
path: node_modules
key: node-modules-${{ matrix.os }}-${{ hashFiles('package.json') }}

- name: Install dependencies
run: npm install

- name: Install browser
run: ./node_modules/.bin/playwright install ${{ fromJSON('{"chrome":"chromium","edge":"msedge","firefox":"firefox","safari":"webkit"}')[matrix.browser] }}

- name: Run Tests on Browsers
run: npm run test:browser ${{ matrix.browser }} ${{ matrix.bundler }}
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -108,3 +108,6 @@ dist
.vscode
docker-compose.yml
dump.rdb

# Temporary files used for browser testing
tmp/
34 changes: 34 additions & 0 deletions README.md
Expand Up @@ -311,6 +311,40 @@ const p1 = cache.fetchSomething(42) // <--- TypeScript doesn't argue anymore her

---

## Browser

All the major browser are supported; only `memory` storage type is supported, `redis` storage can't be used in a browser env.

This is a very simple example of how to use this module in a browser environment:

```html
<script src="https://unpkg.com/async-cache-dedupe"></script>

<script>
const cache = asyncCacheDedupe.createCache({
ttl: 5, // seconds
storage: { type: 'memory' },
})
cache.define('fetchSomething', async (k) => {
console.log('query', k)
return { k }
})
const p1 = cache.fetchSomething(42)
const p2 = cache.fetchSomething(42)
const p3 = cache.fetchSomething(42)
Promise.all([p1, p2, p3]).then((values) => {
console.log(values)
})
</script>
```

You can also use the module with a bundler. The supported bundlers are `webpack`, `rollup`, `esbuild` and `browserify`.

---

## Maintainers

* [__Matteo Collina__](https://github.com/mcollina), <https://twitter.com/matteocollina>, <https://www.npmjs.com/~matteo.collina>
Expand Down
2 changes: 1 addition & 1 deletion bench/storage.js
Expand Up @@ -3,7 +3,7 @@
const { hrtime } = require('process')
const path = require('path')
const Redis = require('ioredis')
const createStorage = require(path.resolve(__dirname, '../storage'))
const createStorage = require(path.resolve(__dirname, '../src/storage/index.js'))

// NOTE: this is a very basic benchmarks for tweaking
// performance is effected by keys and references size
Expand Down
24 changes: 22 additions & 2 deletions package.json
Expand Up @@ -6,6 +6,7 @@
"types": "index.d.ts",
"scripts": {
"test": "standard | snazzy && tap test/*test.js && tsd",
"test:browser": "node test/browser/helpers/runner-browser.mjs",
"lint:fix": "standard --fix",
"redis": "docker run --rm -p 6379:6379 redis redis-server"
},
Expand All @@ -30,15 +31,34 @@
"license": "MIT",
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-inject": "^5.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"browserify": "^17.0.0",
"buffer": "^6.0.3",
"esbuild": "^0.17.4",
"esbuild-plugin-alias": "^0.2.1",
"events": "^3.3.0",
"ioredis": "^5.2.3",
"path-browserify": "^1.0.1",
"playwright": "^1.29.2",
"process": "^0.11.10",
"proxyquire": "^2.1.3",
"rollup": "^3.11.0",
"rollup-plugin-polyfill-node": "^0.12.0",
"snazzy": "^9.0.0",
"standard": "^17.0.0",
"stream-browserify": "^3.0.0",
"tap": "^16.3.0",
"tsd": "^0.25.0"
"tap-mocha-reporter": "^5.0.3",
"tap-parser": "^12.0.1",
"tape": "^5.6.3",
"tsd": "^0.25.0",
"vite": "^4.0.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"abstract-logging": "^2.0.1",
"mnemonist": "^0.39.2",
"safe-stable-stringify": "^2.3.1"
},
Expand Down
11 changes: 10 additions & 1 deletion src/storage/index.js
@@ -1,6 +1,11 @@
'use strict'

const StorageRedis = require('./redis')
const { isServerSide } = require('../util')

let StorageRedis
if (isServerSide) {
StorageRedis = require('./redis')
}
const StorageMemory = require('./memory')

/**
Expand All @@ -27,6 +32,10 @@ const StorageOptionsType = {
* @returns {StorageMemory|StorageRedis}
*/
function createStorage (type, options) {
if (!isServerSide && type === StorageOptionsType.redis) {
throw new Error('Redis storage is not supported in the browser')
}

if (type === StorageOptionsType.redis) {
return new StorageRedis(options)
}
Expand Down
8 changes: 5 additions & 3 deletions src/storage/memory.js
@@ -1,7 +1,7 @@
'use strict'

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

Expand All @@ -26,7 +26,7 @@ class StorageMemory extends StorageInterface {

super(options)
this.size = options.size || DEFAULT_CACHE_SIZE
this.log = options.log || nullLogger
this.log = options.log || abstractLogging()
this.invalidation = options.invalidation || false

this.init()
Expand Down Expand Up @@ -401,7 +401,9 @@ function now () {
return _timer
}
_timer = Math.floor(Date.now() / 1000)
setTimeout(_clearTimer, 1000).unref()
const timeout = setTimeout(_clearTimer, 1000)
// istanbul ignore next
if (typeof timeout.unref === 'function') timeout.unref()
return _timer
}

Expand Down
5 changes: 2 additions & 3 deletions src/storage/redis.js
@@ -1,9 +1,8 @@
'use strict'

const stringify = require('safe-stable-stringify')
const nullLogger = require('abstract-logging')
const StorageInterface = require('./interface')
const { findNotMatching, randomSubset } = require('../util')
const { findNotMatching, randomSubset, abstractLogging } = require('../util')

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

this.log = options.log || nullLogger
this.log = options.log || abstractLogging()
this.store = options.client
this.invalidation = !!options.invalidation
this.referencesTTL = (options.invalidation && options.invalidation.referencesTTL) || REFERENCES_DEFAULT_TTL
Expand Down
21 changes: 19 additions & 2 deletions src/util.js
Expand Up @@ -127,11 +127,28 @@ function wildcardMatch (value, content) {
return i >= value.length - 1
}

// `abstract-logging` dependency has been removed because there is a bug on Rollup
// https://github.com/jsumners/abstract-logging/issues/6
function abstractLogging () {
const noop = () => {}
return {
fatal: noop,
error: noop,
warn: noop,
info: noop,
debug: noop,
trace: noop
}
}

const isServerSide = typeof window === 'undefined'

module.exports = {
findNotMatching,
findMatchingIndexes,
bsearchIndex,
wildcardMatch,

randomSubset
randomSubset,
abstractLogging,
isServerSide
}
7 changes: 7 additions & 0 deletions test/browser/fixtures/esbuild.browser-shims.mjs
@@ -0,0 +1,7 @@
import * as processModule from 'process'

export const process = processModule

export function setImmediate (fn, ...args) {
setTimeout(() => fn(...args), 0)
}
26 changes: 26 additions & 0 deletions test/browser/fixtures/esbuild.browser.config.mjs
@@ -0,0 +1,26 @@
import { build } from 'esbuild'
import alias from 'esbuild-plugin-alias'
import { createRequire } from 'module'

const require = createRequire(import.meta.url)

build({
entryPoints: ['test/browser/test-browser.js'],
outfile: 'tmp/esbuild/suite.browser.js',
bundle: true,
platform: 'browser',
plugins: [
alias({
path: require.resolve('path-browserify'),
stream: require.resolve('stream-browserify')
})
],
define: {
global: 'globalThis'
},
inject: ['test/browser/fixtures/esbuild.browser-shims.mjs'],
external: ['./src/storage/redis.js']
}).catch((err) => {
console.log(err)
process.exit(1)
})
72 changes: 72 additions & 0 deletions test/browser/fixtures/index.html
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
}

#output {
margin: 30px;
white-space: pre;
font-family: monospace;
}

.error {
display: block;
width: calc(100% - 20px);
background-color: #bb0000;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
color: white;
}
</style>
</head>
<body>
<div id="output"></div>

<script type="text/javascript">
let failed = false
const output = document.querySelector('#output')
const originalLog = console.log
const originalError = console.error

console.log = function (message, ...args) {
if (typeof message !== 'string') {
originalLog(message, ...args)
return
}

if (message.includes('not ok')) {
failed = true
document.body.style.backgroundColor = '#ff9d9d'
} else if (message.includes('# async-cache-dedupe-finished') && !failed) {
document.body.style.backgroundColor = '#9dff9d'
}

const span = document.createElement('span')
span.textContent = message + '\n'
output.appendChild(span)
window.scrollTo(0, document.body.scrollHeight)
originalLog(message, ...args)
}

console.error = function (message, ...args) {
if (typeof message !== 'string') {
originalError(message, ...args)
return
}

const span = document.createElement('span')
span.classList.add('error')
span.textContent = message + '\n'
output.appendChild(span)

originalError(message, ...args)
}
</script>
<script type="text/javascript" src="./suite.browser.js"></script>
</body>
</html>

0 comments on commit 341be35

Please sign in to comment.