├── packages ├── arrow-cache │ ├── src │ │ ├── index.ts │ │ ├── server │ │ │ ├── cache │ │ │ │ ├── index.ts │ │ │ │ └── cache.ts │ │ │ ├── db │ │ │ │ ├── index.ts │ │ │ │ └── ice-house.ts │ │ │ ├── index.ts │ │ │ └── store.worker.ts │ │ ├── shared │ │ │ ├── utils │ │ │ │ ├── index.ts │ │ │ │ └── iterator.ts │ │ │ ├── index.ts │ │ │ └── dtos │ │ │ │ ├── index.ts │ │ │ │ ├── cache.ts │ │ │ │ └── keys.ts │ │ ├── client │ │ │ ├── api │ │ │ │ ├── active.ts │ │ │ │ ├── common.ts │ │ │ │ ├── index.ts │ │ │ │ ├── static.ts │ │ │ │ └── base.ts │ │ │ └── main.ts │ │ └── typings │ │ │ └── web-worker.d.ts │ ├── tsconfig.spec.json │ ├── jest.config.js │ ├── tsconfig.json │ ├── package.json │ └── webpack.config.js ├── channel │ ├── src │ │ ├── index.ts │ │ ├── channle-message.ts │ │ ├── client-channel.ts │ │ └── server-channel.ts │ ├── package.json │ └── tsconfig.json └── example │ ├── permanent-counter │ ├── styles.tsx │ ├── index.html │ └── main.tsx │ ├── images.d.ts │ ├── side-effect-update │ ├── index.html │ └── main.tsx │ ├── tsconfig.json │ ├── package.json │ └── common.tsx ├── doc ├── logo.png ├── text.png ├── arrow-cache.png ├── life-circle.png └── README-zh.md ├── .vscode └── settings.json ├── .gitignore ├── lerna.json ├── package.json ├── LICENSE └── README.md /packages/arrow-cache/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client/main"; 2 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/server/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cache"; 2 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ice-house"; 2 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./iterator"; 2 | -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardoc/arrow-cache/HEAD/doc/logo.png -------------------------------------------------------------------------------- /doc/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardoc/arrow-cache/HEAD/doc/text.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.tabSize": 2 4 | } 5 | -------------------------------------------------------------------------------- /doc/arrow-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardoc/arrow-cache/HEAD/doc/arrow-cache.png -------------------------------------------------------------------------------- /doc/life-circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardoc/arrow-cache/HEAD/doc/life-circle.png -------------------------------------------------------------------------------- /packages/arrow-cache/src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./db"; 2 | export * from "./cache"; 3 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dtos"; 2 | export * from "./utils"; 3 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/shared/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cache"; 2 | export * from "./keys"; 3 | -------------------------------------------------------------------------------- /packages/channel/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./server-channel"; 2 | export * from "./client-channel"; 3 | -------------------------------------------------------------------------------- /packages/channel/src/channle-message.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | data?: T; 3 | type: string; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # logs 2 | yarn-error.log 3 | 4 | # dist 5 | dist 6 | 7 | # deps 8 | node_modules 9 | 10 | # parcel cache 11 | .cache -------------------------------------------------------------------------------- /packages/arrow-cache/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/client/api/active.ts: -------------------------------------------------------------------------------- 1 | export interface ActiveAPI { 2 | activeKeys(): Promise; 3 | activeClear(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/client/api/common.ts: -------------------------------------------------------------------------------- 1 | export interface CommonAPI { 2 | innerLength(): Promise; 3 | innerKeys(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/client/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base"; 2 | export * from "./active"; 3 | export * from "./static"; 4 | export * from "./common"; 5 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/client/api/static.ts: -------------------------------------------------------------------------------- 1 | export interface StaticAPI { 2 | staticKeys(): Promise; 3 | staticClear(): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /packages/example/permanent-counter/styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Count = styled.h1` 4 | color: white; 5 | margin: 80px 0; 6 | `; 7 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/shared/dtos/cache.ts: -------------------------------------------------------------------------------- 1 | export interface CacheKey { 2 | key: string; 3 | } 4 | 5 | export interface CacheData extends CacheKey { 6 | content: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/shared/dtos/keys.ts: -------------------------------------------------------------------------------- 1 | export const enum KeysType { 2 | STATIC, 3 | ACTIVE, 4 | ALL 5 | } 6 | 7 | export interface KeysTypeData { 8 | type: KeysType; 9 | } 10 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/typings/web-worker.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.worker.ts" { 2 | class WebpackWorker extends Worker { 3 | constructor(); 4 | } 5 | 6 | export default WebpackWorker; 7 | } 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "independent", 4 | "useWorkspaces": true, 5 | "npmClient": "yarn", 6 | "command": { 7 | "publish": { 8 | "npmClient": "npm" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/channel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@arrow-cache/channel", 3 | "version": "1.0.0", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "compile": "rimraf ./dist && tsc" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/arrow-cache/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | globals: { 5 | "ts-jest": { 6 | tsConfig: "./tsconfig.spec.json" 7 | } 8 | }, 9 | setupFiles: ["jsdom-worker"] 10 | }; 11 | -------------------------------------------------------------------------------- /packages/example/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg"; 2 | 3 | declare module "*.png"; 4 | 5 | declare module "*.jpg"; 6 | 7 | declare module "*.jpeg"; 8 | 9 | declare module "*.gif"; 10 | 11 | declare module "*.bmp"; 12 | 13 | declare module "*.tiff"; 14 | -------------------------------------------------------------------------------- /packages/example/side-effect-update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/arrow-cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "types": ["node"], 8 | "lib": ["ESNext", "WebWorker"], 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "declaration": true 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["node_modules", "typings"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES5", 5 | "rootDir": ".", 6 | "outDir": "dist", 7 | "jsx": "react", 8 | "types": ["react", "react-dom"], 9 | "lib": ["DOM"], 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "declaration": true 13 | }, 14 | "include": ["./**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/example/permanent-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/channel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES6", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "types": ["node"], 8 | "lib": ["ESNext", "WebWorker"], 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "declaration": true, 12 | "strict": true, 13 | "noImplicitAny": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@types/react": "^16.9.23", 8 | "@types/react-dom": "^16.9.5", 9 | "@types/styled-components": "^5.0.1", 10 | "parcel": "^1.12.4", 11 | "react": "^16.13.0", 12 | "react-dom": "^16.13.0", 13 | "styled-components": "^5.0.1" 14 | }, 15 | "dependencies": { 16 | "arrow-cache": "^1.0.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/client/api/base.ts: -------------------------------------------------------------------------------- 1 | export interface StorageAPI { 2 | setItem(key: string, content: string): Promise; 3 | getItem(key: string): Promise; 4 | removeItem(key: string): Promise; 5 | } 6 | 7 | export interface BaseAPI { 8 | markAsActive(key: string): Promise; 9 | markAsStatic(key: string): Promise; 10 | moveToNextStream(key: string): Promise; 11 | updateContent(key: string, content: string): Promise; 12 | clear(): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arrow-cache-packages", 3 | "version": "1.0.4", 4 | "main": "./dist/main.js", 5 | "license": "MIT", 6 | "private": true, 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "yarn workspace @arrow-cache/channel compile && yarn workspace arrow-cache build", 12 | "bootstrap": "lerna bootstrap" 13 | }, 14 | "devDependencies": { 15 | "lerna": "^3.20.2", 16 | "rimraf": "^3.0.2" 17 | }, 18 | "workspaces": { 19 | "packages": [ 20 | "packages/*" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/arrow-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arrow-cache", 3 | "version": "1.0.4", 4 | "main": "./dist/index.js", 5 | "license": "MIT", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "compile": "rimraf ./dist && tsc", 11 | "build": "rimraf ./dist && webpack --mode=production", 12 | "test": "jest" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^25.1.4", 16 | "@types/node": "^13.9.1", 17 | "awesome-typescript-loader": "^5.2.1", 18 | "compression-webpack-plugin": "^3.1.0", 19 | "html-webpack-plugin": "^3.2.0", 20 | "jest": "^25.1.0", 21 | "ts-jest": "^25.2.1", 22 | "typescript": "^3.7.3", 23 | "webpack": "^4.41.2", 24 | "webpack-cli": "^3.3.10", 25 | "webpack-dev-server": "^3.9.0", 26 | "worker-loader": "^2.0.0" 27 | }, 28 | "dependencies": { 29 | "@arrow-cache/channel": "^1.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/example/permanent-counter/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from "react"; 2 | import { render } from "react-dom"; 3 | import { ArrowCache } from "arrow-cache"; 4 | import { Count } from "./styles"; 5 | import { Logo, Global, Button, Container } from "../common"; 6 | 7 | const cache = new ArrowCache({ 8 | isPermanentMemory: true 9 | }); 10 | 11 | const COUNT_KEY = "count_key"; 12 | 13 | const Counter = () => { 14 | const [num, setNum] = useState(0); 15 | 16 | const initNum = async () => setNum(await cache.getItem(COUNT_KEY, 0)); 17 | 18 | const increment = async () => 19 | setNum(await cache.append(COUNT_KEY, pre => pre + 1, 0)); 20 | 21 | useLayoutEffect(() => { 22 | initNum(); 23 | }, []); 24 | 25 | return ( 26 | 27 | 28 | 29 | {num} 30 | 31 | 32 | ); 33 | }; 34 | 35 | render(, document.querySelector("#root")); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Wizaaard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/example/side-effect-update/main.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowCache } from "arrow-cache"; 2 | import { Button } from "../common"; 3 | import React from "react"; 4 | import { render } from "react-dom"; 5 | 6 | const cache = new ArrowCache({ 7 | clearDuration: 1000 8 | }); 9 | 10 | const CACHE_KEY = "foo"; 11 | 12 | const App = () => { 13 | const handleSideEffectClick = () => { 14 | cache.setItem(CACHE_KEY, 0); 15 | 16 | setTimeout(async () => { 17 | console.info(await cache.snapshot()); 18 | 19 | await cache.setItem(CACHE_KEY, 1); 20 | 21 | console.info(await cache.snapshot()); 22 | }, 2100); 23 | }; 24 | 25 | const handlePureClick = () => { 26 | cache.setItem(CACHE_KEY, 0); 27 | 28 | setTimeout(async () => { 29 | console.info(await cache.snapshot()); 30 | 31 | await cache.updateContent(CACHE_KEY, 1); 32 | 33 | console.info(await cache.snapshot()); 34 | }, 2100); 35 | }; 36 | 37 | return ( 38 | <> 39 | 40 |

41 | 42 | 43 | ); 44 | }; 45 | 46 | render(, document.querySelector("#root")); 47 | -------------------------------------------------------------------------------- /packages/example/common.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled, { createGlobalStyle } from "styled-components"; 3 | import LogoImg from "../../doc/logo.png"; 4 | import LogoText from "../../doc/text.png"; 5 | 6 | export const Logo = () => ( 7 |
8 | 9 | 10 |
11 | ); 12 | 13 | export const Container = styled.div` 14 | display: flex; 15 | align-items: center; 16 | flex-direction: column; 17 | `; 18 | 19 | export const Global = createGlobalStyle` 20 | body, html { 21 | height: 100%; 22 | background: #181a1b; 23 | } 24 | `; 25 | 26 | export const Button = styled.div` 27 | height: 40px; 28 | border: 1px solid #3498db; 29 | border-radius: 10000px; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | color: #3498db; 34 | font-size: 13px; 35 | padding: 0 18px; 36 | cursor: pointer; 37 | transition: all 0.3s; 38 | 39 | &:hover { 40 | background: #3498db; 41 | color: white; 42 | } 43 | `; 44 | 45 | export const Container = styled.div` 46 | height: 100%; 47 | display: flex; 48 | margin-top: 100px; 49 | align-items: center; 50 | flex-direction: column; 51 | `; 52 | -------------------------------------------------------------------------------- /packages/channel/src/client-channel.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "./channle-message"; 2 | 3 | export interface PendingMessage { 4 | type: string; 5 | resolver: (value?: R | PromiseLike | undefined) => void; 6 | } 7 | 8 | export class ClientChannel { 9 | private msgQueue: PendingMessage[] = []; 10 | 11 | constructor(private store: Worker) { 12 | this.store.onmessage = this.onMessage as any; 13 | } 14 | 15 | send(payload: Message): Promise { 16 | return new Promise(resolve => { 17 | if (!this.store) { 18 | throw new Error( 19 | "Store cannot be a undefined, please call createClientChannel to resolve it." 20 | ); 21 | } 22 | 23 | this.store.postMessage(payload); 24 | this.msgQueue.push({ 25 | type: payload.type, 26 | resolver: resolve 27 | }); 28 | }); 29 | } 30 | 31 | private onMessage = (e: MessageEvent) => { 32 | const { type, data } = e.data as Message; 33 | 34 | this.unlock(type, data); 35 | }; 36 | 37 | private unlock(targetType: string, data: unknown) { 38 | const pos = this.msgQueue.findIndex( 39 | ({ type }: PendingMessage) => type === targetType 40 | ); 41 | 42 | this.msgQueue[pos].resolver(data); 43 | this.msgQueue.splice(pos, 1); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/arrow-cache/webpack.config.js: -------------------------------------------------------------------------------- 1 | const Path = require("path"); 2 | const CompressionPlugin = require("compression-webpack-plugin"); 3 | 4 | module.exports = { 5 | entry: "./src/index.ts", 6 | output: { 7 | path: Path.join(__dirname, "./dist"), 8 | filename: "index.js", 9 | globalObject: "this", 10 | library: "ArrowCache", 11 | publicPath: "", 12 | libraryTarget: "umd" 13 | }, 14 | resolve: { 15 | extensions: [".ts", ".js"] 16 | }, 17 | plugins: [ 18 | new CompressionPlugin({ 19 | filename: "[path].br[query]", 20 | algorithm: "brotliCompress", 21 | test: /\.(js|css|html|svg)$/, 22 | compressionOptions: { level: 11 }, 23 | threshold: 10240, 24 | minRatio: 0.8, 25 | deleteOriginalAssets: false 26 | }) 27 | ], 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.worker\.ts$/, 32 | use: [ 33 | { 34 | loader: "worker-loader", 35 | options: { 36 | name: "[name]:[hash].js", 37 | inline: true 38 | } 39 | }, 40 | { 41 | loader: "awesome-typescript-loader" 42 | } 43 | ] 44 | }, 45 | { 46 | test: /\.ts$/, 47 | use: "awesome-typescript-loader" 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/shared/utils/iterator.ts: -------------------------------------------------------------------------------- 1 | interface BaseObject { 2 | [key: string]: T; 3 | } 4 | 5 | export function objectMap, P extends keyof T>( 6 | obj: T, 7 | cb: (val: T[P], key: P, obj: T) => T[P] 8 | ): T { 9 | const dup = { ...obj }; 10 | 11 | for (const key of Object.keys(dup)) { 12 | dup[key as P] = cb(dup[key], key as P, dup); 13 | } 14 | 15 | return dup; 16 | } 17 | 18 | export function objectFilter, P extends keyof T>( 19 | obj: T, 20 | cb: (val: T[P], key: P, obj: T) => boolean 21 | ): T { 22 | const dup = { ...obj }; 23 | const result: T = {} as T; 24 | 25 | for (const key of Object.keys(dup)) { 26 | if (cb(dup[key], key as P, dup)) { 27 | result[key as P] = dup[key]; 28 | } 29 | } 30 | 31 | return result; 32 | } 33 | 34 | export function omit(obj: T, prop: P): T { 35 | return Object.keys(obj).reduce( 36 | (total, key) => (key !== prop && (total[key] = obj[key]) && total) || total, 37 | {} 38 | ) as T; 39 | } 40 | 41 | export function findKey( 42 | obj: T, 43 | cb: (item: T[P]) => boolean 44 | ): P { 45 | for (const key of Object.keys(obj)) { 46 | if (cb(obj[key])) { 47 | return key as P; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/channel/src/server-channel.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "./channle-message"; 2 | 3 | export type Res = (resData: unknown) => void; 4 | 5 | type ListenHandler = (data: any | undefined, res: Res) => void; 6 | 7 | export type Self = WorkerGlobalScope & typeof globalThis; 8 | 9 | interface Listener { 10 | eventType: string; 11 | handler: ListenHandler; 12 | } 13 | 14 | export class ServerChannel { 15 | private listeners: Listener[] = []; 16 | 17 | constructor(private self: Self) { 18 | this.self.addEventListener("message", (e: MessageEvent) => 19 | this.onWebWorkerMessage(e.data) 20 | ); 21 | } 22 | 23 | listen(eventType: string, handler: ListenHandler) { 24 | if (!this.self) { 25 | throw new Error("The self is required!"); 26 | } 27 | 28 | this.listeners.push({ 29 | eventType, 30 | handler 31 | }); 32 | } 33 | 34 | private onWebWorkerMessage(msg: Message) { 35 | const listener: Listener | undefined = this.listeners.find( 36 | ({ eventType }: Listener) => eventType === msg.type 37 | ); 38 | 39 | if (listener) { 40 | listener.handler(msg.data, (resData: unknown) => { 41 | if (!this.self) { 42 | throw new Error( 43 | "Self cannot be a undefined, please call createServerChannel to resolve it." 44 | ); 45 | } 46 | 47 | this.self.postMessage({ type: msg.type, data: resData }); 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/server/store.worker.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from "./cache"; 2 | import { CacheData, KeysTypeData, KeysType, CacheKey } from "../shared"; 3 | import { ParsedCacheOptions } from "../client/main"; 4 | import { ServerChannel, Res } from "@arrow-cache/channel"; 5 | 6 | let cache: Cache; 7 | const channel = new ServerChannel(self); 8 | 9 | channel.listen("init", (cacheOptions: ParsedCacheOptions, res: Res) => { 10 | cache = new Cache(cacheOptions); 11 | 12 | res(1); 13 | }); 14 | 15 | channel.listen("permanentMemory", (_, res: Res) => { 16 | cache.stashStore(); 17 | res(1); 18 | }); 19 | 20 | channel.listen("readAllInMemory", async (_, res: Res) => { 21 | await cache.readAllInMemory(); 22 | res(1); 23 | }); 24 | 25 | channel.listen("saveData", async ({ key, content }: CacheData, res: Res) => { 26 | const { iceHouse } = cache; 27 | 28 | // read content of key into memory from icehouse if the key is already exist in icehouse 29 | if ((await cache.coldDataKeys()).includes(key)) { 30 | await iceHouse.remove(key); 31 | } 32 | 33 | cache.addItem(key, content); 34 | 35 | res(1); 36 | }); 37 | 38 | channel.listen("keys", async ({ type }: KeysTypeData, res: Res) => { 39 | const staticKeys = await cache.coldDataKeys(); 40 | const activeKeys = cache.keys; 41 | 42 | const KEYS_MAP = { 43 | [KeysType.STATIC]: staticKeys, 44 | [KeysType.ACTIVE]: activeKeys, 45 | [KeysType.ALL]: Array.from(new Set([...staticKeys, ...activeKeys])) 46 | }; 47 | 48 | res(KEYS_MAP[type]); 49 | }); 50 | 51 | channel.listen("moveToNextStream", async ({ key }: CacheKey, res: Res) => { 52 | if (!cache.findItem(key)) { 53 | const block = await cache.iceHouse.findOnce(key); 54 | 55 | if (!block) { 56 | throw new Error(`Cannot find cache item by ${key}`); 57 | } 58 | 59 | cache.addItem(key, block); 60 | 61 | return res(false); 62 | } 63 | 64 | cache.extendingCacheLife(key); 65 | 66 | res(true); 67 | }); 68 | 69 | channel.listen( 70 | "updateContent", 71 | async ({ key, content }: CacheData, res: Res) => { 72 | const { iceHouse } = cache; 73 | 74 | if (!(await iceHouse.find(key))) { 75 | return res(false); 76 | } 77 | 78 | iceHouse.update(key, content); 79 | res(true); 80 | } 81 | ); 82 | 83 | channel.listen("removeItem", ({ key }: CacheKey, res: Res) => 84 | res(cache.iceHouse.remove(key) || cache.removeItem(key)) 85 | ); 86 | 87 | channel.listen("clear", ({ type }: KeysTypeData, res: Res) => { 88 | const ClEAR_HANDLER_DISPATCHER = { 89 | [KeysType.ACTIVE]: () => cache.clear(), 90 | [KeysType.STATIC]: () => cache.iceHouse.clear(), 91 | [KeysType.ALL]: () => { 92 | cache.clear(); 93 | cache.iceHouse.clear(); 94 | } 95 | }; 96 | 97 | res(ClEAR_HANDLER_DISPATCHER[type]()); 98 | }); 99 | 100 | channel.listen( 101 | "mark", 102 | async ({ type, key }: KeysTypeData & CacheKey, res: Res) => { 103 | type MarkHandlerDispatcher = { 104 | [type in KeysType]: () => Promise | boolean; 105 | }; 106 | 107 | const MARK_HANDLER_DISPATCHER: Partial = { 108 | [KeysType.STATIC]: () => { 109 | const content = cache.findOnce(key); 110 | 111 | if (!content) { 112 | return false; 113 | } 114 | 115 | cache.iceHouse.add(key, content); 116 | return true; 117 | }, 118 | [KeysType.ACTIVE]: async () => { 119 | const content = await cache.iceHouse.findOnce(key); 120 | 121 | if (!content) { 122 | return false; 123 | } 124 | 125 | cache.addItem(key, content); 126 | 127 | return true; 128 | } 129 | }; 130 | 131 | res(await MARK_HANDLER_DISPATCHER[type]!()); 132 | } 133 | ); 134 | 135 | channel.listen("snapshot", async (_, res: Res) => res(await cache.snapshot())); 136 | 137 | channel.listen("getItem", async ({ key, content }: CacheData, res: Res) => { 138 | const memoryItem = cache.findItem(key); 139 | 140 | if (memoryItem) { 141 | return res(memoryItem); 142 | } 143 | 144 | const diskItem = await cache.iceHouse.find(key); 145 | 146 | if (!diskItem) { 147 | // write the default value into memory 148 | if (content !== undefined) { 149 | cache.addItem(key, content); 150 | 151 | return res(content); 152 | } 153 | 154 | return res(undefined); 155 | } 156 | 157 | cache.iceHouse.remove(key); 158 | cache.addItem(key, diskItem); 159 | 160 | res(diskItem); 161 | }); 162 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/server/db/ice-house.ts: -------------------------------------------------------------------------------- 1 | export interface IIceHouse { 2 | add(key: string, content: string): Promise; 3 | update(key: string, content: string): Promise; 4 | remove(key: string): Promise; 5 | find(key: string): Promise; 6 | batchAdd(items: ColdDataItem[]): Promise; 7 | } 8 | 9 | export interface ColdDataItem { 10 | key: string; 11 | content: string; 12 | } 13 | 14 | export interface ObjectifyColdData { 15 | [key: string]: string; 16 | } 17 | 18 | export type ParsedAllColdData = T extends true 19 | ? ObjectifyColdData 20 | : ColdDataItem[]; 21 | 22 | export class IceHouse implements IIceHouse { 23 | private initPromise: Promise; 24 | 25 | private readonly DATABASE_NAME = "ice_house"; 26 | private readonly TABLE_NAME = "cold_data"; 27 | 28 | constructor() { 29 | const request = indexedDB.open(this.DATABASE_NAME); 30 | 31 | request.onerror = err => console.error(err); 32 | // recreate table when db is upgrade 33 | request.onupgradeneeded = e => 34 | this.handleIceHouseInit((e.target as any).result); 35 | 36 | this.initPromise = new Promise(resolve => { 37 | request.onsuccess = e => { 38 | resolve((e.target as any).result); 39 | }; 40 | }); 41 | } 42 | 43 | private handleIceHouseInit(db: IDBDatabase) { 44 | const coldData = db.createObjectStore(this.TABLE_NAME, { keyPath: "key" }); 45 | 46 | coldData.createIndex("key", "key", { unique: true }); 47 | 48 | return coldData; 49 | } 50 | 51 | // return a objectStore when db connect success 52 | private async getObjectStore(): Promise { 53 | const db = await this.initPromise; 54 | 55 | return db.transaction(["cold_data"], "readwrite").objectStore("cold_data"); 56 | } 57 | 58 | // wrap db operate fn 59 | private getResult(request: IDBRequest): Promise { 60 | return new Promise((resolve, reject) => { 61 | request.onsuccess = e => { 62 | resolve((e.target as any).result); 63 | }; 64 | 65 | request.onerror = err => { 66 | reject(err); 67 | }; 68 | }); 69 | } 70 | 71 | // set a data in db 72 | async add(key: string, content: string): Promise { 73 | const coldData = await this.getObjectStore(); 74 | 75 | // invoke update method when throw doublekey 76 | try { 77 | await this.getResult(coldData.add({ key, content })); 78 | } catch (e) { 79 | this.update(key, content); 80 | } 81 | } 82 | 83 | async remove(key: string): Promise { 84 | if (!(await this.keys()).includes(key)) { 85 | return false; 86 | } 87 | 88 | const coldData = await this.getObjectStore(); 89 | 90 | return this.getResult(coldData.delete(key)); 91 | } 92 | 93 | async update(key: string, content: string): Promise { 94 | const coldData = await this.getObjectStore(); 95 | 96 | return this.getResult(coldData.put({ key, content })); 97 | } 98 | 99 | async find(key: string): Promise { 100 | const coldData = await this.getObjectStore(); 101 | const result: ColdDataItem = await this.getResult(coldData.get(key)); 102 | 103 | if (!result) { 104 | return undefined; 105 | } 106 | 107 | return result.content; 108 | } 109 | 110 | async batchAdd(items: ColdDataItem[]): Promise { 111 | const coldData = await this.getObjectStore(); 112 | 113 | return Promise.all(items.map(item => this.getResult(coldData.put(item)))); 114 | } 115 | 116 | async keys(): Promise { 117 | const coldData = await this.getObjectStore(); 118 | 119 | return this.getResult(coldData.getAllKeys()); 120 | } 121 | 122 | async findOnce(key: string): Promise { 123 | let result: string | undefined; 124 | 125 | if ((await this.keys()).includes(key)) { 126 | result = await this.find(key); 127 | this.remove(key); 128 | } 129 | 130 | return result; 131 | } 132 | 133 | async clear(): Promise { 134 | const coldData = await this.getObjectStore(); 135 | 136 | return this.getResult(coldData.clear()); 137 | } 138 | 139 | async findAll(isObjectify?: true): Promise; 140 | async findAll(isObjectify?: false): Promise; 141 | async findAll( 142 | isObjectify?: boolean 143 | ): Promise { 144 | const coldData = await this.getObjectStore(); 145 | const result = await this.getResult(coldData.getAll()); 146 | 147 | return isObjectify || true 148 | ? result.reduce( 149 | (total, cur) => (total[cur.key] = cur.content) && total, 150 | {} 151 | ) 152 | : result; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/server/cache/cache.ts: -------------------------------------------------------------------------------- 1 | import { objectMap, objectFilter, omit } from "../../shared"; 2 | import { IceHouse, ColdDataItem, ParsedAllColdData } from "../db"; 3 | import { ParsedCacheOptions } from "../../client/main"; 4 | 5 | interface ICache { 6 | addItem(key: string, data: string): boolean; 7 | removeItem(key: string): boolean; 8 | updateItem(key: string, data: string): boolean; 9 | findItem(key: string): string | undefined; 10 | } 11 | 12 | /** 13 | * key-value store for cache some data 14 | */ 15 | interface CacheItem { 16 | content: string; 17 | // write the item on disk when lifeCount = 0 18 | lifeCount: number; 19 | // mark whether cacheItem is active. When isActivated is True, the cacheItem is exist in memory, 20 | // otherwise on disk 21 | isActivated: boolean; 22 | // mark the item can be cleared 23 | imminentDead?: boolean; 24 | } 25 | 26 | export interface Store { 27 | [key: string]: CacheItem; 28 | } 29 | 30 | export interface Snapshot { 31 | memory: Store; 32 | disk: ParsedAllColdData; 33 | } 34 | 35 | type Timeout = number; 36 | 37 | const INIT_LIFECOUNT = 2; 38 | 39 | export class Cache implements ICache { 40 | private store: Store = {}; 41 | private currentTimerId: Timeout | undefined; 42 | private clearDuration: number; 43 | private _iceHouse: IceHouse; 44 | private isPermanentMemory: boolean; 45 | 46 | constructor(options: ParsedCacheOptions) { 47 | this._iceHouse = new IceHouse(); 48 | this.clearDuration = options.clearDuration; 49 | this.isPermanentMemory = options.isPermanentMemory; 50 | // create a timer for monitor lifeCount and clear the cache block that's dead 51 | this.initMonitor(); 52 | } 53 | 54 | // write in disk 55 | private killCache(items: ColdDataItem): void; 56 | private killCache(items: ColdDataItem[]): void; 57 | private killCache(items: ColdDataItem[] | ColdDataItem): void { 58 | if ((items as ColdDataItem[]).length) { 59 | this.iceHouse.batchAdd(items as ColdDataItem[]); 60 | } else { 61 | const item = items as ColdDataItem; 62 | 63 | this.iceHouse.add(item.key, item.content); 64 | } 65 | } 66 | 67 | private cacheMonitor() { 68 | const imminentDeadItems: ColdDataItem[] = []; 69 | 70 | // check lifeCount and mark imminentDead 71 | this.store = objectMap(this.store, (val: CacheItem) => ({ 72 | ...val, 73 | lifeCount: val.lifeCount - 1, 74 | imminentDead: val.lifeCount - 1 === 0 75 | })); 76 | 77 | for (const key of this.keys) { 78 | const item = this.store[key]; 79 | 80 | if (this.store[key].imminentDead) { 81 | imminentDeadItems.push({ 82 | key, 83 | content: item.content 84 | }); 85 | } 86 | } 87 | 88 | // clear the item in memory if it's lifeCount = 0 89 | this.store = objectFilter( 90 | this.store, 91 | (val: CacheItem) => !val.imminentDead 92 | ); 93 | 94 | // stop timer when there's nothing in store 95 | if (!this.keys.length) { 96 | clearInterval(this.currentTimerId); 97 | 98 | this.currentTimerId = undefined; 99 | } 100 | 101 | // processing imminentDeadItems 102 | if (imminentDeadItems.length) { 103 | this.killCache(imminentDeadItems); 104 | } 105 | } 106 | 107 | private initMonitor() { 108 | this.currentTimerId = (setInterval( 109 | () => this.cacheMonitor(), 110 | this.clearDuration 111 | ) as unknown) as Timeout; 112 | } 113 | 114 | addItem(key: string, data: string): boolean { 115 | this.store[key] = { 116 | content: data, 117 | lifeCount: INIT_LIFECOUNT, 118 | isActivated: true 119 | }; 120 | 121 | if (this.isPermanentMemory) { 122 | this.iceHouse.add(key, data); 123 | } 124 | 125 | // addItem will add item to the store 126 | // create a timer when current timer is destroy 127 | if (!this.currentTimerId) { 128 | this.initMonitor(); 129 | } 130 | 131 | return true; 132 | } 133 | 134 | removeItem(key: string): boolean { 135 | if (!this.store[key]) { 136 | return false; 137 | } 138 | 139 | this.killCache({ key, content: this.store[key].content }); 140 | this.store = omit(this.store, key); 141 | 142 | return true; 143 | } 144 | 145 | updateItem(key: string, data: string): boolean { 146 | if (!this.store[key]) { 147 | return false; 148 | } 149 | 150 | this.store[key].content = data; 151 | } 152 | 153 | findItem(key: string): string | undefined { 154 | const item = this.store[key]; 155 | 156 | if (!item) { 157 | return; 158 | } 159 | 160 | return this.store[key].content; 161 | } 162 | 163 | findOnce(key: string): string | undefined { 164 | const item = this.findItem(key); 165 | 166 | this.removeItem(key); 167 | 168 | return item; 169 | } 170 | 171 | coldDataKeys(): Promise { 172 | return this.iceHouse.keys(); 173 | } 174 | 175 | extendingCacheLife(key: string) { 176 | this.store[key].lifeCount++; 177 | } 178 | 179 | clear() { 180 | this.store = {}; 181 | } 182 | 183 | stashStore() { 184 | this.iceHouse.batchAdd( 185 | this.keys.map(key => ({ key, content: this.store[key].content })) 186 | ); 187 | } 188 | 189 | async readAllInMemory() { 190 | const diskAll = ((await this.iceHouse.findAll()) as unknown) as Store; 191 | 192 | this.store = objectMap(diskAll, (item: CacheItem) => ({ 193 | content: (item as unknown) as string, 194 | lifeCount: 2, 195 | isActivated: true 196 | })); 197 | // this.iceHouse.clear(); 198 | } 199 | 200 | get keys(): string[] { 201 | return Object.keys(this.store); 202 | } 203 | 204 | async snapshot(): Promise> { 205 | return { 206 | memory: { ...this.store }, 207 | disk: { ...(await this.iceHouse.findAll()) } 208 | }; 209 | } 210 | 211 | get iceHouse(): IceHouse { 212 | return this._iceHouse; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /doc/README-zh.md: -------------------------------------------------------------------------------- 1 |
2 |

arrow cache

3 |
4 |

5 | 6 |

7 |

8 | 9 | 10 | 11 |

12 | 13 | [English](/README.md) | 中文文档 14 | 15 | # Arrow Cache 16 | 17 | 基于 `WebWorker` 的缓存机制,提供可靠高性能的缓存,帮助构建高性能的 `webApp`。 18 | 19 | ## 特性 20 | 21 | 👋 类型安全,arrow Cache 用 ts 编写,对代码提示和类型约束有良好的支持。 22 | 23 | 🚀 高性能,对缓存库的操作都是异步的,背后是 Worker thread 在帮忙处理一切的存取操作。 24 | 25 | 🍰 控制内存,通过简单有效的算法筛选数据,保证内存中热数据缓存的占比。 26 | 27 | 🍷 丰富的 API 对缓存的可控性,提供一系列操作缓存的细粒度的方法,能更有效的控制缓存的生命周期。 28 | 29 | 🌲 数据持久化,对于冷数据会被 arrow cache 从内存中移除持久化到硬盘,有需要的时候再读入内存,提高访问速度。 30 | 31 | ## 简介 32 | 33 | `Arrow Cache` 和许多缓存库一样,通过 `Key-Value` 来缓存数据,任何存入 `Arrow Cache` 的数据都会被第一时间放入内存。当放入的东西越来越多,内存会逐渐膨胀,`Arrow Cache` 会启动一个定时器来每隔一段时间做一次标记清除,这种做法会使得内存里都是热数据,进而控制内存。这些被标记的数据并不会马上从内存里被清除,而是持久化到硬盘,下次再用到这个数据的时候会先从内存里寻找,如果没有找到,会发起一起 IO 从硬盘上寻找,如果发现会将它读入内存并且初始化。 34 | 35 | ## 缓存的生命周期 36 | 37 | 每个存入 `Cache Store` 的数据都会有一个 `isActive` 标记和一个 `lifeCount` 标记。 38 | 39 | `isActive` 40 | 41 | 当数据存在于内存中时,证明它是活跃的,这时候该数据的 `isActive` 是 `TRUE`,如果它被写入了磁盘,这时候变为 `FALSE` 代表它是不活跃的。 42 | 43 | `lifeCount` 44 | 45 | 用来帮助 `Arrow Cache` 的标记清除机制,每隔一段时间(默认是 10min,可以通过设置 `clearDuration` 来更改)会检查当前容器中的数据的 `lifeCount` 是否为 0(初始化是 2),当 `lifeCount` 为 0 时,会将这个数据标记为 `imminentDead` 表示为可以被清除的,然后会将所有标记为 `imminentDead` 的数据持久化到磁盘。 46 | 47 | ## Usage 48 | 49 | ```bash 50 | # yarn 51 | yarn add arrow-cache 52 | # npm 53 | npm install arrow-cache 54 | ``` 55 | 56 | ```js 57 | /** Usage */ 58 | import { ArrowCache } from "arrow-cache"; 59 | 60 | const cache = new ArrowCache(); 61 | 62 | cache.setItem("name", "Jon"); 63 | ``` 64 | 65 | ## Constructor Options 66 | 67 | ``` 68 | ArrowCache(Options) 69 | ``` 70 | 71 | 你可以在实例化 `ArrowCache` 的时候传递一个 Options,可用属性如下: 72 | 73 | `isPermanentMemory` `[Boolean]` 74 | 75 | 标记是否为“常驻内存”,如果开启此项,`Arrow Cache` 会在你每次 setItem 的时候都持久化到硬盘,下一次刷新页面的时候将它读到内存,调用者会感受到它一直存在于内存中。 76 | 77 | `clearDuration` `[Number]` 78 | 79 | 设置清理周期,`Arrow Cache` 每隔一段时间会清理掉生命计数为 0 的缓存项。 80 | 81 | ## 使用默认值 82 | 83 | 为避免做一些冗余的非空判断,我们为个别方法提供了默认值,例如: 84 | 85 | ```typescript 86 | import { ArrowCache } from "arrow-cache"; 87 | 88 | const cache = new ArrowCache(); 89 | 90 | // 👎 91 | const doSomething = async () => { 92 | const foo = await cache.getItem("foo"); 93 | 94 | if (!foo) { 95 | cache.setItem("foo", 0); 96 | } 97 | 98 | // ... 99 | }; 100 | 101 | // 👍 102 | const doSomething = async () => { 103 | const foo = await cache.getItem("foo", 0); 104 | 105 | // ... 106 | }; 107 | 108 | // 👍 109 | const doSomething = async () => { 110 | const foo = await cache.append("foo", foo => foo + 1, 0); 111 | 112 | // ... 113 | }; 114 | ``` 115 | 116 | ## 更新的副作用 117 | 118 | `Arrow Cache` 提供了一些更新缓存项的方法,`setItem` 和 `updateContent`。我们称 `setItem` 是有副作用的,而 `updateContent` 是没有副作用的,`setItem` 在 `key` 对应的项不存在的时候会自动创建一个新的,而 `updateContent` 则会返回 false 并且不会自动创建。另一个区别是,当目标缓存项已经被写在硬盘并且被标记为 cold Data 时,使用 `setItem` 会使该缓存项被标记为 `active` 并且读入内存,而 `updateContent` 不论缓存项在内存还是硬盘都不会改变状态。 119 | 120 | ```typescript 121 | ////////////////////////// SIDE EFFECT ///////////////////////// 122 | 123 | cache.setItem(CACHE_KEY, 0); 124 | 125 | setTimeout(async () => { 126 | console.info(await cache.snapshot()); // {memory: {}, disk: {foo: "0"}} 127 | 128 | await cache.setItem(CACHE_KEY, 1); 129 | 130 | console.info(await cache.snapshot()); // {memory: {foo: {content: "1", lifeCount: 2, isActivated: true}}, disk: {}} 131 | }, 2100); 132 | 133 | cache.setItem(CACHE_KEY, 0); 134 | 135 | //////////////////////////// PURE //////////////////////////// 136 | 137 | setTimeout(async () => { 138 | console.info(await cache.snapshot()); // {memory: {}, disk: {foo: "0"}} 139 | 140 | await cache.updateContent(CACHE_KEY, 1); 141 | 142 | console.info(await cache.snapshot()); // {memory: {}, disk: {foo: "1"}} 143 | }, 2100); 144 | ``` 145 | 146 | ## 何时读入内存 147 | 148 | Arrow Cache 会在构造函数调用的时候通过 `Worker` 将硬盘上的数据读入内存,这一过程为 Arrow Cache 的初始化,你可以通过传递一个回调在它初始化完成后做一些事情。 149 | 150 | ```typescript 151 | import { ArrowCache } from "arrow-cache"; 152 | 153 | const cache = new ArrowCache(); 154 | 155 | cache.onInit(() => { 156 | // do something... 157 | }); 158 | ``` 159 | 160 | ## 控制缓存项的生命周期 161 | 162 | 掌控缓存项的生命周期能更好的控制缓存的性能和当前情况。 163 | 164 | ![life-circle](https://raw.githubusercontent.com/wizardoc/arrow-cache/master/doc/life-circle.png) 165 | 166 | 我们提供了三个 API 来帮助你控制缓存项的生命周期. 167 | 168 | ```typescript 169 | moveToNextStream(key: string): Promise 170 | ``` 171 | 172 | `moveToNextStream` 能够让指定的 key 对应的缓存项进入下一个清理周期。lifeCount 是影响缓存项是否活跃的唯一指标,lifeCount 会加一,随之进入下一个清理周期。如果 key 对应的缓存项不存在,则会返回 false。 173 | 174 | ```typescript 175 | markAsActive(key: string): Promise 176 | ``` 177 | 178 | `markAsActive` 帮助对应的缓存项从硬盘读入内存。如果 key 对应的缓存项不存在,则会返回 false。 179 | 180 | ```typescript 181 | markAsStatic(key: string): Promise 182 | ``` 183 | 184 | `markAsStatic` 会让对应的缓存项从内存写到硬盘。如果 key 对应的缓存项不存在,则会返回 false。 185 | 186 | ## keys 187 | 188 | 我们提供了一组 keys 的方法,可以轻松拿到所在空间的所有的 keys。 189 | 190 | ```typescript 191 | activeKeys(): Promise 192 | ``` 193 | 194 | `activeKeys` 将内存中所有的缓存项的 keys 返回出来。也意味着内存中所有的缓存项都是活跃的。 195 | 196 | ```typescript 197 | staticKeys(): Promise 198 | ``` 199 | 200 | `staticKeys` 将硬盘中所有的缓存项的 keys 返回出来。也意味着硬盘中所有的缓存项都是不活跃的。 201 | 202 | ```typescript 203 | keys(): Promise 204 | ``` 205 | 206 | `keys` 方法返回所有的 keys(包含内存和硬盘) 207 | 208 | ## Debug 209 | 210 | 有时候,需要准确的知道内存中的缓存的情况。可以使用 `snapshot` 方法打印当前缓存的状况 211 | 212 | ```typescript 213 | import { ArrowCache } from "arrow-cache"; 214 | 215 | const cache = new ArrowCache(); 216 | 217 | (async () => { 218 | console.info(await cache.snapshot()); // {memory: {}, disk: {}} 219 | })(); 220 | ``` 221 | 222 | `snapshot` 返回当前时间点缓存的快照,它是对内存的一层浅拷贝 223 | 224 | ## Examples 225 | 226 | 一些例子在 [Examples](packages/example) 下,通过 `npx parcel index.html` 即可启动 227 | 228 | ### 持久化计数 229 | 230 | [Counter](packages/example/permanent-counter/main.tsx) 231 | 232 | ```tsx 233 | import React, { useLayoutEffect, useState } from "react"; 234 | import { render } from "react-dom"; 235 | import { ArrowCache } from "arrow-cache"; 236 | import { Count } from "./styles"; 237 | import { Logo, Global, Button, Container } from "../common"; 238 | 239 | const cache = new ArrowCache({ 240 | isPermanentMemory: true 241 | }); 242 | 243 | const COUNT_KEY = "count_key"; 244 | 245 | const Counter = () => { 246 | const [num, setNum] = useState(0); 247 | 248 | const initNum = async () => setNum(await cache.getItem(COUNT_KEY, 0)); 249 | 250 | const increment = async () => 251 | setNum(await cache.append(COUNT_KEY, pre => pre + 1, 0)); 252 | 253 | useLayoutEffect(() => { 254 | initNum(); 255 | }, []); 256 | 257 | return ( 258 | 259 | 260 | 261 | {num} 262 | 263 | 264 | ); 265 | }; 266 | 267 | render(, document.querySelector("#root")); 268 | ``` 269 | 270 | ### 副作用更新 271 | 272 | [Side Effect Update](packages/example/side-effect-update/main.tsx) 273 | 274 | ```tsx 275 | import { ArrowCache } from "arrow-cache"; 276 | import { Button } from "../common"; 277 | import React from "react"; 278 | import { render } from "react-dom"; 279 | 280 | const cache = new ArrowCache({ 281 | clearDuration: 1000 282 | }); 283 | 284 | const CACHE_KEY = "foo"; 285 | 286 | const App = () => { 287 | const handleSideEffectClick = () => { 288 | cache.setItem(CACHE_KEY, 0); 289 | 290 | setTimeout(async () => { 291 | console.info(await cache.snapshot()); 292 | 293 | await cache.setItem(CACHE_KEY, 1); 294 | 295 | console.info(await cache.snapshot()); 296 | }, 2100); 297 | }; 298 | 299 | const handlePureClick = () => { 300 | cache.setItem(CACHE_KEY, 0); 301 | 302 | setTimeout(async () => { 303 | console.info(await cache.snapshot()); 304 | 305 | await cache.updateContent(CACHE_KEY, 1); 306 | 307 | console.info(await cache.snapshot()); 308 | }, 2100); 309 | }; 310 | 311 | return ( 312 | <> 313 | 314 |

315 | 316 | 317 | ); 318 | }; 319 | 320 | render(, document.querySelector("#root")); 321 | ``` 322 | 323 | ## APIs 324 | 325 | [v1.0.0](https://github.com/wizaaard/arrow-cache/tree/master/apis/v1.0.0) 326 | 327 | # LICENSE 328 | 329 | MIT. 330 | -------------------------------------------------------------------------------- /packages/arrow-cache/src/client/main.ts: -------------------------------------------------------------------------------- 1 | import StoreWorker from "../server/store.worker.ts"; 2 | import { KeysTypeData, KeysType, CacheData, CacheKey } from "../shared"; 3 | import { Snapshot } from "../server"; 4 | import { ClientChannel } from "@arrow-cache/channel"; 5 | 6 | export interface ParsedCacheItemOptions { 7 | /** 8 | * when lifeCount of the cacheItem is 0, it will be written in disk, 9 | * therefore if you want keep this item always in memory, you have to set isOnlyMemory to TRUE 10 | * the cache item will always exist in memory when isOnlyMemory is TRUE 11 | */ 12 | isOnlyMemory: boolean; 13 | /** 14 | * please noted that is same like isOnlyMemory, but there are some subtle difference between them. 15 | * the cacheItem will not be mark to cold data if it's lifeCount is 0 when isAlwaysActive is TRUE 16 | */ 17 | isAlwaysActive: boolean; 18 | } 19 | 20 | export interface ParsedCacheOptions { 21 | /** 22 | * if isPermanentMemory is TRUE, setItem will written cache data in disk, the cacheItem will not be 23 | * recycling when refresh the page 24 | */ 25 | isPermanentMemory: boolean; 26 | /** 27 | * time duration to clear cold data 28 | */ 29 | clearDuration: number; 30 | } 31 | 32 | export type CacheOptions = Partial; 33 | export type CacheItemOptions = Partial; 34 | 35 | /** 36 | * the types of data allow to be stored. 37 | * if the type of cacheData is not exist in AllowStorageTypes, 38 | * you must transfer the type to string manually. 39 | */ 40 | type AllowStorageTypes = boolean | string | number | object | Array; 41 | 42 | export class ArrowCache { 43 | private store: StoreWorker; 44 | private channel: ClientChannel; 45 | private cacheOptions: ParsedCacheOptions; 46 | private initPromise: Promise; 47 | 48 | constructor(options?: CacheOptions) { 49 | this.store = new StoreWorker(); 50 | this.channel = new ClientChannel(this.store); 51 | this.cacheOptions = this.parseCacheOptions(options ?? {}); 52 | 53 | this.initPromise = new Promise(resolve => this.init(resolve)); 54 | } 55 | 56 | async init(resolve: () => void) { 57 | this.sendMsg("init", this.cacheOptions); 58 | 59 | if (this.cacheOptions.isPermanentMemory) { 60 | await this.sendMsg("readAllInMemory"); 61 | 62 | resolve(); 63 | } 64 | } 65 | 66 | private sendMsg(type: string, data?: T): Promise { 67 | return this.channel.send({ 68 | type, 69 | data: data ?? {} 70 | }); 71 | } 72 | 73 | private parseOptions(defaultOptions: T, options: T) { 74 | return { 75 | ...defaultOptions, 76 | ...(options ?? {}) 77 | }; 78 | } 79 | 80 | private parseCacheOptions(options: CacheOptions): ParsedCacheOptions { 81 | return this.parseOptions( 82 | { 83 | isPermanentMemory: false, 84 | clearDuration: 20000 85 | }, 86 | options 87 | ) as ParsedCacheOptions; 88 | } 89 | 90 | private parseCacheItemOptions( 91 | options: CacheItemOptions 92 | ): ParsedCacheItemOptions { 93 | return this.parseOptions( 94 | { 95 | isOnlyMemory: false, 96 | isAlwaysActive: false 97 | }, 98 | options 99 | ) as ParsedCacheItemOptions; 100 | } 101 | 102 | /** 103 | * set a listener when read all data into memory 104 | */ 105 | async onInit(cb: () => void) { 106 | await this.initPromise; 107 | 108 | cb(); 109 | } 110 | 111 | /** 112 | * get all keys of cache block including disk 113 | */ 114 | activeKeys(): Promise { 115 | return this.sendMsg("keys", { 116 | type: KeysType.ACTIVE 117 | }); 118 | } 119 | /** 120 | * get all keys of cache block including disk 121 | */ 122 | staticKeys(): Promise { 123 | return this.sendMsg("keys", { 124 | type: KeysType.STATIC 125 | }); 126 | } 127 | /** 128 | * get all keys of cache block including memory and disk 129 | */ 130 | keys(): Promise { 131 | return this.sendMsg("keys", { 132 | type: KeysType.ALL 133 | }); 134 | } 135 | /** 136 | * clear all cache blocks from disk 137 | */ 138 | activeClear(): Promise { 139 | return this.sendMsg("clear", { key: KeysType.ACTIVE }); 140 | } 141 | /** 142 | * clear all cache blocks from memory 143 | */ 144 | staticClear(): Promise { 145 | return this.sendMsg("clear", { key: KeysType.STATIC }); 146 | } 147 | /** 148 | * clear all cache blocks, include memory and disk 149 | */ 150 | clear(): Promise { 151 | return this.sendMsg("clear", { key: KeysType.ALL }); 152 | } 153 | /** 154 | * mark the isActivated of the cache block as true and move it into memory 155 | */ 156 | markAsActive(key: string): Promise { 157 | return this.sendMsg("mark", { type: KeysType.ACTIVE, key }); 158 | } 159 | /** 160 | * mark the isActivated of the cache block as false and move it into iceHouse, 161 | * BTW, the isActivated field just exist in memory. 162 | * The iceHouse data structure looks like following: 163 | * 164 | * interface ColdDataItem { 165 | * key: string; 166 | * content: string; 167 | * } 168 | * 169 | */ 170 | markAsStatic(key: string): Promise { 171 | return this.sendMsg("mark", { type: KeysType.STATIC, key }); 172 | } 173 | /** 174 | * setItem will set a cache block in memory if the key does not exist in cache, 175 | * otherwise, arrow-cache will read the cache block into memory and update content 176 | * of the cache block. 177 | */ 178 | setItem( 179 | key: string, 180 | content: AllowStorageTypes, 181 | options?: CacheItemOptions 182 | ): Promise { 183 | return this.sendMsg("saveData", { 184 | key, 185 | content: JSON.stringify(content), 186 | options: this.parseCacheItemOptions(options ?? {}) 187 | }); 188 | } 189 | /** 190 | * append a new value to an existing cacheItem 191 | */ 192 | async append( 193 | key: string, 194 | cb: (data: T) => T, 195 | defaultValue: T 196 | ): Promise; 197 | async append( 198 | key: string, 199 | cb: (data: T | undefined) => T, 200 | defaultValue?: T | undefined 201 | ): Promise; 202 | async append( 203 | key: string, 204 | cb: (data: T | undefined) => T, 205 | defaultValue: T | undefined 206 | ): Promise { 207 | const val = await this.getItem(key, defaultValue as T); 208 | const appendVal = cb(val as any); 209 | 210 | this.setItem(key, appendVal); 211 | 212 | return appendVal; 213 | } 214 | /** 215 | * get cache block from memory or disk 216 | */ 217 | async getItem( 218 | key: string, 219 | defaultValue: AllowStorageTypes 220 | ): Promise; 221 | async getItem( 222 | key: string 223 | ): Promise; 224 | async getItem( 225 | key: string, 226 | defaultValue?: AllowStorageTypes 227 | ): Promise { 228 | const content: string = await this.sendMsg("getItem", { 229 | key, 230 | content: defaultValue 231 | }); 232 | let result: AllowStorageTypes; 233 | 234 | try { 235 | result = JSON.parse(content); 236 | } catch (e) { 237 | result = content; 238 | } 239 | 240 | return result as T; 241 | } 242 | /** 243 | * remove the cache block of key from memory or disk 244 | */ 245 | removeItem(key: string): Promise { 246 | return this.sendMsg("removeItem", { key }); 247 | } 248 | /** 249 | * Each cache block has a lifeCount attribute, and every once in a while lifeCount will be -1, 250 | * arrow-cache will remove cache block whose is lifeCount = 0 from memory and write it in disk, 251 | * arrow-cache will read it into memory when use the cache block next time. 252 | * moveToNextStream will move the cache block of key in next clear-circle. 253 | * So you can keep extending the life of cache block. 254 | */ 255 | moveToNextStream(key: string): Promise { 256 | return this.sendMsg("moveToNextStream", { key }); 257 | } 258 | /** 259 | * updateContent is a idempotent method that will update content of key. 260 | * if the cache block is exist in icehouse, updateContent does not make it as active. 261 | */ 262 | updateContent(key: string, content: AllowStorageTypes): Promise { 263 | return this.sendMsg("updateContent", { 264 | key, 265 | content: JSON.stringify(content) 266 | }); 267 | } 268 | 269 | snapshot(): Promise> { 270 | return this.sendMsg("snapshot"); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

arrow cache

3 |
4 |

5 | 6 |

7 |

8 | 9 | 10 | 11 |

12 | 13 | English | [中文文档](doc/README-zh.md) 14 | 15 | # Arrow Cache 16 | 17 | Cache mechanism base on Web Worker, help us build high performance `webApp`. 18 | 19 | ## TL;DR 20 | 21 | 👋 Type-Safer. `Arrow Cache` is written in TypeScript and good support for code hints and type constraints. 22 | 23 | 🚀 High performance. all operations on cache library is asynchronous, the reason is that behind it is Worker Thread that handle all the storage access operations. 24 | 25 | 🍰 Less memory. the cache items filter by simple and effective algorithm to ensure the proportion of hot data in memory. 26 | 27 | 🍷 Rich API. provide a series methods for manipulate the cache store to control the life-circle of cache item effective. 28 | 29 | 🌲Data persistence. `Arrow Cache` will remove it and from memory and persist it to disk if it's lifeCount eq to 0.If I access it again, it will be read in memory to improve speed for next access. 30 | 31 | ## Overview 32 | 33 | `Arrow Cache` use key-value to cache data like any other cache library. Any data stored in `Arrow Cache` will be put into memory immediately.But the amount of data you put in is proportional to the amount of memory you use, `Arrow Cache` will create a timer to mark-clean at regular interval for keep hot data in memory.The marked data will be persist in disk and remove it in memory. `Arrow Cache` will quest this data in memory first when access it next time, if not found, Worker will create a IO request to look for it from the disk, and if found it, this data will be read in memory and initialize. 34 | 35 | ## The Life-Circle of Cache 36 | 37 | Each data store in `Cache store` has a `isActive` tag and `lifeCount` tag. 38 | 39 | `isActive` 40 | 41 | Data is active when it already in memory, in this case, the `isActive` of this data is `TRUE`. The `isActive` of this data will go from `TRUE` to `FALSE` if it is written to disk, in this case, this data is not active anymore. 42 | 43 | `lifeCount` 44 | 45 | LifeCount is core of mark-clean mechanism, `Arrow Cache` will check the lifeCount for each data in Cache Store is 0 (default is 2) at regular interval(default is 10min and can be change it by setting `clearDuration` option). `Arrow Cache` will mark this data as `imminentDead` to indicate that it can be cleared when the `lifeCount` of this data is 0, then `Arrow Cache` will persisted all data with `imminentDead` tag to disk. 46 | 47 | ## Usage 48 | 49 | ```bash 50 | # yarn 51 | yarn add arrow-cache 52 | # npm 53 | npm install arrow-cache 54 | ``` 55 | 56 | ```js 57 | /** Usage */ 58 | import { ArrowCache } from "arrow-cache"; 59 | 60 | const cache = new ArrowCache(); 61 | 62 | cache.setItem("name", "Jon"); 63 | ``` 64 | 65 | ## Constructor Options 66 | 67 | ``` 68 | ArrowCache(Options) 69 | ``` 70 | 71 | You can pass a options when create instance of `ArrowCache`. The available properties are as follows: 72 | 73 | `isPermanentMemory` `[Boolean]` 74 | 75 | Mark cache as "Permanent Memory". If the `isPermanentMemory` is true, `Arrow Cache` will persist the data in disk when you invoke `setItem` every time. And read it into memory when refresh the page next time. As a caller, you will feel that it's always in memory. 76 | 77 | `clearDuration` `[Number]` 78 | 79 | Set the cleaning circle. `Arrow Cache` will clear data which lifeCount is 0 at regular interval. 80 | 81 | ## Use Default Value 82 | 83 | To avoid making redundant non-null assertion, We provide default values for individual method, for example: 84 | 85 | ```typescript 86 | import { ArrowCache } from "arrow-cache"; 87 | 88 | const cache = new ArrowCache(); 89 | 90 | // 👎 91 | const doSomething = async () => { 92 | const foo = await cache.getItem("foo"); 93 | 94 | if (!foo) { 95 | cache.setItem("foo", 0); 96 | } 97 | 98 | // ... 99 | }; 100 | 101 | // 👍 102 | const doSomething = async () => { 103 | const foo = await cache.getItem("foo", 0); 104 | 105 | // ... 106 | }; 107 | 108 | // 👍 109 | const doSomething = async () => { 110 | const foo = await cache.append("foo", foo => foo + 1, 0); 111 | 112 | // ... 113 | }; 114 | ``` 115 | 116 | ## Side Effect Of Updates 117 | 118 | `Arrow Cache` provide some methods to update the content of cache, for example, `setItem` and `updateContent`. We say `setItem` has side effect and `updateContent` has not side effect. `setItem` will create a new item in Cache Store when content of key does not exist. But `updateContent` will return false and is not automatically created a new item in the Cache Store. The other difference is `setItem` will mark this data as `active` and read in memory when the data is already written in disk and mark as `hot data`, but `updateContent` does not change the state of data whether the data is in memory or on the disk. 119 | 120 | ```typescript 121 | ////////////////////////// SIDE EFFECT ///////////////////////// 122 | 123 | cache.setItem(CACHE_KEY, 0); 124 | 125 | setTimeout(async () => { 126 | console.info(await cache.snapshot()); // {memory: {}, disk: {foo: "0"}} 127 | 128 | await cache.setItem(CACHE_KEY, 1); 129 | 130 | console.info(await cache.snapshot()); // {memory: {foo: {content: "1", lifeCount: 2, isActivated: true}}, disk: {}} 131 | }, 2100); 132 | 133 | cache.setItem(CACHE_KEY, 0); 134 | 135 | //////////////////////////// PURE //////////////////////////// 136 | 137 | setTimeout(async () => { 138 | console.info(await cache.snapshot()); // {memory: {}, disk: {foo: "0"}} 139 | 140 | await cache.updateContent(CACHE_KEY, 1); 141 | 142 | console.info(await cache.snapshot()); // {memory: {}, disk: {foo: "1"}} 143 | }, 2100); 144 | ``` 145 | 146 | ## Read data into memory from Disk 147 | 148 | Arrow Cache will read data into memory from disk by Worker Thread when create instance, This procedure initializes Arrow Cache.You can pass a callback to `onInit` if you want to do something after initializes Arrow Cache. 149 | 150 | ```typescript 151 | import { ArrowCache } from "arrow-cache"; 152 | 153 | const cache = new ArrowCache(); 154 | 155 | cache.onInit(() => { 156 | // do something... 157 | }); 158 | ``` 159 | 160 | ## Control Circle-Life of Data 161 | 162 | Mastering life-circle of cache item can better control performance and current situation of the Cache. 163 | 164 | ![life-circle](https://raw.githubusercontent.com/wizardoc/arrow-cache/master/doc/life-circle.png) 165 | 166 | We provide three APIs to help you control life-circle of cache item. 167 | 168 | ```typescript 169 | moveToNextStream(key: string): Promise 170 | ``` 171 | 172 | `moveToNextStream` can move cache item of key to next clear-circle. `lifeCount` is the only factor that influence whether the cache item is active. `moveToNextStream` will cause `lifeCount` of the item + 1 and move it to next clear-circle. But if `moveToNextStream` return false indicate that the item of key does not exist. 173 | 174 | ```typescript 175 | markAsActive(key: string): Promise 176 | ``` 177 | 178 | `markAsActive` can read the item of key in memory, the method will return false when the item of key does not exist. 179 | 180 | ```typescript 181 | markAsStatic(key: string): Promise 182 | ``` 183 | 184 | `markAsStatic` can write the item of ket on the disk, the method will return false when the item of key does not exist. 185 | 186 | ## Keys 187 | 188 | We provide a series methods that easily get all keys of the cache store. 189 | 190 | ```typescript 191 | activeKeys(): Promise 192 | ``` 193 | 194 | `activeKeys` return all keys of data in the cache, and also means that all cache item in memory is active. 195 | 196 | ```typescript 197 | staticKeys(): Promise 198 | ``` 199 | 200 | `staticKeys` return all keys of data on the disk, and also means that all cache item on the disk is not active. 201 | 202 | ```typescript 203 | keys(): Promise 204 | ``` 205 | 206 | `keys` return all keys of cache whether the data is in memory or on the disk. 207 | 208 | ## Debug 209 | 210 | Sometimes, we need to know situation of cache in memory, therefore we can print snapshot of cache by invoke `snapshot` method. 211 | 212 | ```typescript 213 | import { ArrowCache } from "arrow-cache"; 214 | 215 | const cache = new ArrowCache(); 216 | 217 | (async () => { 218 | console.info(await cache.snapshot()); // {memory: {}, disk: {}} 219 | })(); 220 | ``` 221 | 222 | `snapshot` method return snapshot of cache at current point in time. The `snapshot` method will return a new object that is a shallow-copy of memory. 223 | 224 | ## Examples 225 | 226 | We have some examples under the [Examples](packages/example), you can start the example by `npx parcel index.html` 227 | 228 | ### permanent Counter 229 | 230 | [Counter](packages/example/permanent-counter/main.tsx) 231 | 232 | ```tsx 233 | import React, { useLayoutEffect, useState } from "react"; 234 | import { render } from "react-dom"; 235 | import { ArrowCache } from "arrow-cache"; 236 | import { Count } from "./styles"; 237 | import { Logo, Global, Button, Container } from "../common"; 238 | 239 | const cache = new ArrowCache({ 240 | isPermanentMemory: true 241 | }); 242 | 243 | const COUNT_KEY = "count_key"; 244 | 245 | const Counter = () => { 246 | const [num, setNum] = useState(0); 247 | 248 | const initNum = async () => setNum(await cache.getItem(COUNT_KEY, 0)); 249 | 250 | const increment = async () => 251 | setNum(await cache.append(COUNT_KEY, pre => pre + 1, 0)); 252 | 253 | useLayoutEffect(() => { 254 | initNum(); 255 | }, []); 256 | 257 | return ( 258 | 259 | 260 | 261 | {num} 262 | 263 | 264 | ); 265 | }; 266 | 267 | render(, document.querySelector("#root")); 268 | ``` 269 | 270 | ### Side Effect Update 271 | 272 | [Side Effect Update](packages/example/side-effect-update/main.tsx) 273 | 274 | ```tsx 275 | import { ArrowCache } from "arrow-cache"; 276 | import { Button } from "../common"; 277 | import React from "react"; 278 | import { render } from "react-dom"; 279 | 280 | const cache = new ArrowCache({ 281 | clearDuration: 1000 282 | }); 283 | 284 | const CACHE_KEY = "foo"; 285 | 286 | const App = () => { 287 | const handleSideEffectClick = () => { 288 | cache.setItem(CACHE_KEY, 0); 289 | 290 | setTimeout(async () => { 291 | console.info(await cache.snapshot()); 292 | 293 | await cache.setItem(CACHE_KEY, 1); 294 | 295 | console.info(await cache.snapshot()); 296 | }, 2100); 297 | }; 298 | 299 | const handlePureClick = () => { 300 | cache.setItem(CACHE_KEY, 0); 301 | 302 | setTimeout(async () => { 303 | console.info(await cache.snapshot()); 304 | 305 | await cache.updateContent(CACHE_KEY, 1); 306 | 307 | console.info(await cache.snapshot()); 308 | }, 2100); 309 | }; 310 | 311 | return ( 312 | <> 313 | 314 |

315 | 316 | 317 | ); 318 | }; 319 | 320 | render(, document.querySelector("#root")); 321 | ``` 322 | 323 | ## APIs 324 | 325 | [v1.0.0](https://github.com/wizaaard/arrow-cache/tree/master/apis/v1.0.0) 326 | 327 | # LICENSE 328 | 329 | MIT. 330 | --------------------------------------------------------------------------------