Repository: KID-joker/proxy-web-storage Branch: main Commit: 62f6ee4db5f3 Files: 36 Total size: 91.6 KB Directory structure: gitextract_0wk62674/ ├── .eslintignore ├── .gitignore ├── LICENSE ├── README.ja.md ├── README.md ├── README.zh.md ├── eslint.config.js ├── package.json ├── playground/ │ └── template.js ├── playwright.config.ts ├── rollup.config.js ├── src/ │ ├── extends/ │ │ ├── disposable.ts │ │ ├── expires.ts │ │ ├── options.ts │ │ └── watch.ts │ ├── index.ts │ ├── proxy/ │ │ ├── broadcast.ts │ │ ├── object.ts │ │ ├── storage.ts │ │ └── transform.ts │ ├── shared.ts │ ├── types.ts │ └── utils.ts ├── tests/ │ ├── base.spec.ts │ ├── disposable.spec.ts │ ├── equal.spec.ts │ ├── expired.spec.ts │ ├── global.d.ts │ ├── localforage.spec.ts │ ├── serializer.spec.ts │ ├── storage.spec.ts │ ├── storageOptions.spec.ts │ └── subscribe.spec.ts ├── tsconfig.json ├── v2.md └── v2.zh.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ .DS_Store node_modules dist playground/stokado.js ================================================ FILE: .gitignore ================================================ .DS_Store node_modules dist playground/index.html playground/stokado.js test-results .vscode ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 KID-joker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.ja.md ================================================ ```shell __ __ __ __ ____ /\ \__ ___ /\ \/ \ __ /\ \ ___ / ,__\ \ \ ,_\ / __`\ \ \ < /'__`\ \_\ \ / __`\ /\__, `\ \ \ \/ /\ \_\ \ \ \ ^ \ /\ \_\.\_ /\ ,. \ /\ \_\ \ \/\____/ \ \ \_ \ \____/ \ \_\ \_\\ \__/.\_\\ \____\\ \____/ \/___/ \ \__\ \/___/ \/_/\/_/ \/__/\/_/ \/___ / \/___/ \/__/ ``` **[English](./README.md) | [中文](./README.zh.md) | 日本語** [v2 ドキュメント](./v2.md) > *Stokado*(/stəˈkɑːdoʊ/) は *storage* の[エスペラント語](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%B9%E3%83%9A%E3%83%A9%E3%83%B3%E3%83%88)(国際補助語)であり、*Stokado* は *storage* の補助エージェントでもあります。 `stokado` は、任意の `storage` ライクなオブジェクトをプロキシし、ゲッター/セッターのシンタックスシュガー、シリアライゼーション、サブスクリプションリスニング、期限設定、一度だけの値の取得を提供します。 ## 使用方法 ### インストール ```shell npm install stokado ``` ### プロキシ ```js import { createProxyStorage } from 'stokado' const storage = createProxyStorage(localStorage) storage.getItem('test') ``` #### createProxyStorage(storage[, name]) `createProxyStorage` は2つのパラメータを取ります: `storage` ライクなオブジェクトとオプションの `name`。`name` は他のページと `storage` の変更を同期するために使用されます。デフォルトでは、`localStorage` は同じ `name` を持ちますが、`sessionStorage` は持ちません。他のオブジェクトの場合は手動で渡す必要があります。 ### 機能 #### 1. シンタックスシュガー オブジェクト指向のアプローチで直接 `storage` を操作します もちろん、`localStorage` と `sessionStorage` はネイティブにサポートされています ```js const storage = createProxyStorage(localStorage) storage.test = 'hello stokado' storage.test // 'hello stokado' delete storage.test ``` `storage` には同じメソッドとプロパティもあります: `key()`, `getItem()`, `setItem()`, `removeItem()`, `clear()`, `length`。 #### 2. シリアライザー ストレージ値の型を変更せずに保持します ```js // number storage.test = 0 storage.test === 0 // boolean storage.test = false storage.test === false // undefined storage.test = undefined storage.test === undefined // null storage.test = null storage.test === null // object storage.test = { hello: 'world' } storage.test.hello === 'stokado' // array storage.test = ['hello'] storage.test.push('stokado') storage.test.length // 2 // Date storage.test = new Date('2000-01-01T00:00:00.000Z') storage.test.getTime() === 946684800000 // RegExp storage.test = /d(b+)d/g storage.test.test('cdbbdbsbz') // function storage.test = function () { return 'hello stokado!' } storage.test() === 'hello stokado!' ``` #### 3. サブスクライブ 値の変更をサブスクライブします ```js storage.on(key, callback) storage.once(key, callback) storage.off([[key], callback]) ``` - `key`: サブスクライブするアイテムの名前。`Object` の `obj.a` や `Array` の `list[0]`、および `Array` の長さをサポートします。 - `callback`: アイテムが変更されたときに呼び出される関数。`newValue` と `oldValue` を含みます。 **ヒント:** `off` の場合、`callback` が存在する場合は指定されたコールバックのトリガーを削除します。存在しない場合は、`key` にバインドされたすべてのコールバックを削除します。`key` が空の場合は、すべてのリスニングコールバックを削除します。 #### 4. 期限 アイテムの期限を設定します ```js storage.setExpires(key, expires) storage.getExpires(key) storage.removeExpires(key) ``` - `key`: 期限を設定するアイテムの名前。 - `expires`: `string`、`number`、`Date` を受け入れます。 #### 5. 一度だけ 一度だけ値を取得します。これは `storage` を介して通信するために使用できます。 ```js storage.setDisposable(key) ``` - `key`:一度だけの値を設定するアイテムの名前。 #### 6. オプション 指定されたアイテムの `expires` と `disposable` の設定情報を取得します ```js storage.getOptions(key) ``` `setItem` を使用して `expires` と `disposable` を設定します ```js storage.setItem(key, value, { expires, disposable }) ``` ## localForage と一緒に使う `localForage` は `localStorage` と同じ API を提供しているため、`stokado` と一緒に使用できます。 ```js import localForage from 'localforage' import { createProxyStorage } from 'stokado' const local = createProxyStorage(localForage, 'localForage') ``` ただし、`localForage` は非同期 API を使用しているため、`Promise` を使用して呼び出す必要があります。 ```js await (local.test = 'hello localForage') // または await local.setItem('test', 'hello localForage') ``` #### 複数のインスタンス `createInstance` を使用して、異なるストアを指す `localForage` の複数のインスタンスを作成できます。 ```js const store = localforage.createInstance({ name: 'nameHere' }) const proxyStore = createProxyStorage(store, 'store') const otherStore = localforage.createInstance({ name: 'otherName' }) const proxyOtherStore = createProxyStorage(otherStore, 'otherStore') ``` ================================================ FILE: README.md ================================================ ```shell __ __ __ __ ____ /\ \__ ___ /\ \/ \ __ /\ \ ___ / ,__\ \ \ ,_\ / __`\ \ \ < /'__`\ \_\ \ / __`\ /\__, `\ \ \ \/ /\ \_\ \ \ \ ^ \ /\ \_\.\_ /\ ,. \ /\ \_\ \ \/\____/ \ \ \_ \ \____/ \ \_\ \_\\ \__/.\_\\ \____\\ \____/ \/___/ \ \__\ \/___/ \/_/\/_/ \/__/\/_/ \/___ / \/___/ \/__/ ``` **English | [中文](./README.zh.md) | [日本語](./README.ja.md)** [v2 document](./v2.md) > *Stokado*(/stəˈkɑːdoʊ/) is the [Esperanto](https://en.wikipedia.org/wiki/Esperanto)(an international auxiliary language) for *storage*, meaning that *Stokado* is also an auxiliary agent for *storage*. `stokado` can proxy objects of any `storage`-like, providing getter/setter syntax sugars, serialization, subscription listening, expiration setting, one-time value retrieval. ## Usage ### Install ```shell npm install stokado ``` ### Proxy ```js import { createProxyStorage } from 'stokado' const storage = createProxyStorage(localStorage) storage.getItem('test') ``` #### createProxyStorage(storage[, name]) `createProxyStorage` takes two parameters: an object of `storage`-like and an optional `name`. The `name` is used to synchronize `storage` modifications with other pages. By default, `localStorage` has the same `name`, whereas `sessionStorage` does not; for other objects, it needs to be passed in manually. ### Features #### 1. Syntax sugar Operate `storage` directly through object-oriented approach Of course, `localStorage` and `sessionStorage` are supported natively ```js const storage = createProxyStorage(localStorage) storage.test = 'hello stokado' storage.test // 'hello stokado' delete storage.test ``` The `storage` also have the same methods and properties: `key()`, `getItem()`, `setItem()`, `removeItem()`, `clear()` and `length`. #### 2. Serializer Keep the type of storage value unchanged ```js // number storage.test = 0 storage.test === 0 // boolean storage.test = false storage.test === false // undefined storage.test = undefined storage.test === undefined // null storage.test = null storage.test === null // object storage.test = { hello: 'world' } storage.test.hello === 'stokado' // array storage.test = ['hello'] storage.test.push('stokado') storage.test.length // 2 // Date storage.test = new Date('2000-01-01T00:00:00.000Z') storage.test.getTime() === 946684800000 // RegExp storage.test = /d(b+)d/g storage.test.test('cdbbdbsbz') // function storage.test = function () { return 'hello stokado!' } storage.test() === 'hello stokado!' ``` #### 3. Subscribe Subscribe to value changes ```js storage.on(key, callback) storage.once(key, callback) storage.off([[key], callback]) ``` - `key`: the name of the item to subscribe to. Support `obj.a` for `Object` and `list[0]` for `Array`, and also `Array` length. - `callback`: the function to call when the item is changed. Includes `newValue` and `oldValue`. **Tips:** For `off`, if a `callback` exists, it removes the trigger of the specified callback; otherwise, it removes all callbacks bound to the `key`; if the `key` is empty, it removes all listening callbacks. #### 4. Expired Set expires for items ```js storage.setExpires(key, expires) storage.getExpires(key) storage.removeExpires(key) ``` - `key`: the name of the item to set expires. - `expires`: accept `string`、`number` and `Date`. #### 5. Disposable Get the value once, which can be used for communication through `storage`. ```js storage.setDisposable(key) ``` - `key`:the name of the item to set disposable. #### 6. Options Get `expires` and `disposable` configuration information for the specified item ```js storage.getOptions(key) ``` Set `expires` and `disposable` using `setItem` ```js storage.setItem(key, value, { expires, disposable }) ``` ## Work with localForage `localForage` provides the same API as `localStorage`, it can be used in conjunction with `stokado`. ```js import localForage from 'localforage' import { createProxyStorage } from 'stokado' const local = createProxyStorage(localForage, 'localForage') ``` However, `localForage` uses an async API, it needs to be called using `Promise`. ```js await (local.test = 'hello localForage') // or await local.setItem('test', 'hello localForage') ``` #### Multiple instances You can create multiple instances of `localForage` that point to different stores using `createInstance`. ```js const store = localforage.createInstance({ name: 'nameHere' }) const proxyStore = createProxyStorage(store, 'store') const otherStore = localforage.createInstance({ name: 'otherName' }) const proxyOtherStore = createProxyStorage(otherStore, 'otherStore') ``` ================================================ FILE: README.zh.md ================================================ ```shell __ __ __ __ ____ /\ \__ ___ /\ \/ \ __ /\ \ ___ / ,__\ \ \ ,_\ / __`\ \ \ < /'__`\ \_\ \ / __`\ /\__, `\ \ \ \/ /\ \_\ \ \ \ ^ \ /\ \_\.\_ /\ ,. \ /\ \_\ \ \/\____/ \ \ \_ \ \____/ \ \_\ \_\\ \__/.\_\\ \____\\ \____/ \/___/ \ \__\ \/___/ \/_/\/_/ \/__/\/_/ \/___ / \/___/ \/__/ ``` **[English](./README.md) | 中文 | [日本語](./README.ja.md)** [v2 文档](./v2.zh.md) > *stokado*(/stəˈkɑːdoʊ/) 是 *storage* 的[世界语](https://zh.wikipedia.org/wiki/%E4%B8%96%E7%95%8C%E8%AF%AD)(一种国际辅助语言),喻意为 *stokado* 也是 *storage* 的辅助代理。 `stokado` 可以代理任何类 `storage` 的对象,实现简洁的 `getter`,`setter` 等语法糖,序列化,监听订阅,设置过期,一次性取值等功能。 ## Usage ### Install ```shell npm install stokado ``` ### Proxy ```js import { createProxyStorage } from 'stokado' const storage = createProxyStorage(localStorage) storage.getItem('test') ``` #### createProxyStorage(storage[, name]) `createProxyStorage` 接收两个参数:类 `storage` 对象和可选的 `name`。`name` 用于同步其他页面的 `storage` 修改。`localStorage` 默认存在同名的 `name`,`sessionStorage` 则没有,其他对象需自行传入。 ### Features #### 1. Syntax sugar 通过对象方式直接操作 `storage` 当然,`localStorage` 和 `sessionStorage` 本身也是支持的 ```js const storage = createProxyStorage(localStorage) storage.test = 'hello stokado' storage.test // 'hello stokado' delete storage.test ``` 同时也支持 `storage` 的原生方法和属性:`key()`,`getItem()`,`setItem()`,`removeItem()`,`clear()` 和 `length`。 #### 2. Serializer 保持值类型不变 ```js // number storage.test = 0 storage.test === 0 // boolean storage.test = false storage.test === false // undefined storage.test = undefined storage.test === undefined // null storage.test = null storage.test === null // object storage.test = { hello: 'world' } storage.test.hello === 'stokado' // array storage.test = ['hello'] storage.test.push('stokado') storage.test.length // 2 // Date storage.test = new Date('2000-01-01T00:00:00.000Z') storage.test.getTime() === 946684800000 // RegExp storage.test = /d(b+)d/g storage.test.test('cdbbdbsbz') // function storage.test = function () { return 'hello stokado!' } storage.test() === 'hello stokado!' ``` #### 3. Subscribe 监听储值的变化 ```js storage.on(key, callback) storage.once(key, callback) storage.off([[key], callback]) ``` - `key`:监听指定项的名字。支持对象的二级监听,例如:`obj.a` 对于 `Object` 和 `list[0]` 对于 `Array`,还支持数组长度的监听。 - `callback`:指定项的值发生变化时,触发的回调函数。参数包括`newValue` 和 `oldValue`。 **Tips:** 对于 `off`,如果 `callback` 存在,则移除指定回调的触发;否则,移除对于 `key` 绑定的所有回调;如果 `key` 为空,移除所有监听回调。 #### 4. Expired 为指定项设置过期时间 ```js storage.setExpires(key, expires) storage.getExpires(key) storage.removeExpires(key) ``` - `key`:设置过期的指定项名字。 - `expires`:过期时间。接受`string`、`number` 和 `Date`类型。 #### 5. Disposable 一次性取值,可用于借助 `storage` 进行通信 ```js storage.setDisposable(key) ``` - `key`:设置一次性的指定项名字。 #### 6. Options 获取指定项的过期、一次性等配置信息 ```js storage.getOptions(key) ``` 通过 `setItem` 设置过期及一次性 ```js storage.setItem(key, value, { expires, disposable }) ``` ## Work with localForage 因为 `localForage` 提供了跟 `localStorage` 一样的 API,它是类 `storage` 对象,可以跟 `stokado` 配合使用。 ```js import localForage from 'localforage' import { createProxyStorage } from 'stokado' const local = createProxyStorage(localForage, 'localForage') ``` 但是因为 `localForage` 采用异步的 API,所以需要使用 `Promise` 来调用它。 ```js await (local.test = 'hello localForage') // or await local.setItem('test', 'hello localForage') ``` #### Multiple instances 通过 `createInstance` 可以创建多个 `localForage` 实例,也是类 `storage` 对象。 ```js const store = localforage.createInstance({ name: 'nameHere' }) const proxyStore = createProxyStorage(store, 'store') const otherStore = localforage.createInstance({ name: 'otherName' }) const proxyOtherStore = createProxyStorage(otherStore, 'otherStore') ``` ================================================ FILE: eslint.config.js ================================================ import antfu from '@antfu/eslint-config' export default antfu({ ignores: ['.DS_Store', '**/.DS_Store/**', 'node_modules', 'dist', 'playground/stokado.js'], }, { rules: { 'no-new-wrappers': 'off', 'prefer-regex-literals': 'off', 'ts/no-unsafe-function-type': 'off', 'unicorn/new-for-builtins': 'off', }, }) ================================================ FILE: package.json ================================================ { "name": "stokado", "type": "module", "version": "3.0.1", "description": "stokado can proxy objects of any `storage`-like, providing getter/setter syntax sugars, serialization, subscription listening, expiration setting, one-time value retrieval.", "author": "KID-joker", "license": "MIT", "homepage": "https://github.com/KID-joker/stokado#readme", "repository": { "type": "git", "url": "git+https://github.com/KID-joker/stokado.git" }, "bugs": { "url": "https://github.com/KID-joker/stokado/issues" }, "keywords": [ "localStorage", "sessionStorage", "storage", "browser", "proxy", "serializer", "subscribe", "expires", "once", "disposable" ], "exports": { ".": { "types": "./dist/stokado.d.ts", "import": "./dist/stokado.mjs", "require": "./dist/stokado.cjs" } }, "main": "dist/stokado.cjs", "module": "dist/stokado.mjs", "unpkg": "dist/stokado.min.js", "jsdelivr": "dist/stokado.min.js", "types": "dist/stokado.d.ts", "files": [ "dist" ], "engines": { "node": ">=16.20.0" }, "scripts": { "build": "rimraf dist && rollup -c --environment BUILD:prod", "dev": "rollup -c -w --environment BUILD:dev", "test": "rollup -c --environment BUILD:test && npx playwright test", "lint": "eslint . --fix", "typecheck": "tsc --noEmit" }, "devDependencies": { "@antfu/eslint-config": "^5.2.2", "@playwright/test": "^1.55.0", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-html": "^2.0.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-typescript": "^12.1.4", "@types/node": "^24.3.1", "esbuild": "^0.25.9", "eslint": "^9.35.0", "http-server": "^14.1.1", "lint-staged": "^16.1.6", "rimraf": "^6.0.1", "rollup": "^4.50.0", "rollup-plugin-dts": "^6.2.3", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-serve": "^3.0.0", "simple-git-hooks": "^2.13.1", "tslib": "^2.8.1", "typescript": "^5.9.2" }, "simple-git-hooks": { "pre-commit": "npx lint-staged" }, "lint-staged": { "*.{js,ts,md}": [ "eslint --fix" ] } } ================================================ FILE: playground/template.js ================================================ function template(options) { return ` ${options.title} ` } export default template ================================================ FILE: playwright.config.ts ================================================ import type { PlaywrightTestConfig } from '@playwright/test' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) const root = path.resolve(__dirname, './playground') const port = 8080 const config: PlaywrightTestConfig = { testDir: 'tests', webServer: { command: `npx http-server ${root} -p ${port} --cors`, port, reuseExistingServer: !process.env.CI, }, use: { baseURL: `http://localhost:${port}`, }, projects: [{ name: 'chromium', use: { browserName: 'chromium', }, }], } export default config ================================================ FILE: rollup.config.js ================================================ import { readFileSync } from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import alias from '@rollup/plugin-alias' import html from '@rollup/plugin-html' import { nodeResolve } from '@rollup/plugin-node-resolve' import typescript from '@rollup/plugin-typescript' import dts from 'rollup-plugin-dts' import esbuild from 'rollup-plugin-esbuild' import serve from 'rollup-plugin-serve' import template from './playground/template.js' const pkg = JSON.parse(readFileSync('./package.json', { encoding: 'utf8' })) const __dirname = fileURLToPath(new URL('.', import.meta.url)) const configs = [] const srcDir = path.resolve(__dirname, 'src') const input = path.resolve(__dirname, 'src/index.ts') const pkgName = pkg.name const output = [{ file: `${process.env.BUILD === 'prod' ? 'dist' : 'playground'}/${pkgName}.js`, format: 'iife', name: pkgName, extend: true, }] const pluginEsbuild = process.env.BUILD === 'prod' ? esbuild({ drop: ['console'] }) : esbuild() const pluginAlias = alias({ entries: [{ find: '@', replacement: srcDir }] }) const plugins = [pluginEsbuild, typescript({ declaration: false }), pluginAlias, nodeResolve({ browser: true })] if (process.env.BUILD === 'prod') { output.push({ file: `dist/${pkgName}.mjs`, format: 'es', }, { file: `dist/${pkgName}.cjs`, format: 'cjs', }, { file: `dist/${pkgName}.min.js`, format: 'iife', name: pkgName, extend: true, plugins: [ esbuild({ minify: true, }), ], }) configs.push({ input, output: { file: `dist/${pkgName}.d.ts`, format: 'es', }, plugins: [ dts(), pluginAlias, ], }) } if (process.env.BUILD !== 'prod') { plugins.push(html({ title: pkgName, template, })) } if (process.env.BUILD === 'dev') plugins.push(serve('playground')) configs.push({ input, output, plugins, }) export default configs ================================================ FILE: src/extends/disposable.ts ================================================ import type { StorageObject } from '@/types' import { encode } from '@/proxy/transform' import { deleteProxyStorageProperty, getProxyStorageProperty } from '@/shared' import { isObject, pThen } from '@/utils' export function setDisposable( storage: Record, property: string, ) { pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { if (isObject(res)) { const options = Object.assign({}, res?.options, { disposable: true }) const encodeValue = encode({ data: res.value, storage, property, options }) storage.setItem(property, encodeValue) } }) } export function checkDisposable({ data, storage, property, }: { data: StorageObject | string | null storage: Record property: string }) { if (!isObject(data) || !data.options) return data const { disposable } = data.options if (disposable) { deleteProxyStorageProperty(storage, property) } return data } ================================================ FILE: src/extends/expires.ts ================================================ import type { ExpiresType, StorageObject, StorageOptions } from '@/types' import { encode } from '@/proxy/transform' import { deleteProxyStorageProperty, getProxyStorageProperty } from '@/shared' import { formatTime, isObject, pThen } from '@/utils' import { getOptions } from './options' export function setExpires( storage: Record, property: string, expires: ExpiresType, ) { const time = formatTime(expires) if (time <= Date.now()) { deleteProxyStorageProperty(storage, property) return undefined } pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { if (isObject(res)) { const options = Object.assign({}, res?.options, { expires: time }) const encodeValue = encode({ data: res.value, storage, property, options }) storage.setItem(property, encodeValue) } }) } export function getExpires( storage: Record, property: string, ) { return pThen(() => getOptions(storage, property), (res: StorageOptions) => { if (!res?.expires || +res.expires <= Date.now()) return undefined return new Date(+res.expires) }) } export function removeExpires( storage: Record, property: string, ) { pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { if (isObject(res) && res.options) { delete res.options.expires const encodeValue = encode({ data: res.value, storage, property, options: res.options }) storage.setItem(property, encodeValue) } }) } export function checkExpired({ data, storage, property, }: { data: StorageObject | string | null storage: Record property: string }) { if (!isObject(data) || !data.options) return data const { expires } = data.options if (expires && new Date(+expires).getTime() <= Date.now()) { deleteProxyStorageProperty(storage, property) data.value = undefined } return data } ================================================ FILE: src/extends/options.ts ================================================ import type { StorageObject } from '@/types' import { getProxyStorageProperty } from '@/shared' import { isObject, pThen } from '@/utils' export function getOptions( storage: Record, property: string, ) { return pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { if (isObject(res) && res.options) return res.options return {} }) } ================================================ FILE: src/extends/watch.ts ================================================ import type { Effect, EffectFn, EffectMap } from '@/types' import { postMessage } from '@/proxy/broadcast' import { hasChanged } from '@/utils' const storageEffectMap = new WeakMap() export function on( this: any, storage: object, key: string, fn: EffectFn, ) { const effect: Effect = { ctx: this, fn, } let effectMap = storageEffectMap.get(storage) if (!effectMap) storageEffectMap.set(storage, (effectMap = new Map())) const effects: Effect[] | undefined = effectMap.get(key) if (effects) effects.push(effect) else effectMap.set(key, [effect] as Effect[]) } export function once( this: any, storage: object, key: string, fn: EffectFn, ) { const wrapped = (value: any, oldValue: any) => { off(storage, key, wrapped) fn.call(this, value, oldValue) } // in order to filter wrapped.fn = fn on(storage, key, wrapped) } export function off( storage: object, key?: string, fn?: EffectFn, ) { if (key === undefined) { storageEffectMap.set(storage, new Map()) return } const effectMap: EffectMap | undefined = storageEffectMap.get(storage) if (effectMap) { const effects: Effect[] | undefined = effectMap.get(key) if (effects && effects.length > 0) { const value: Effect[] = fn ? effects.filter(ele => !(ele.fn === fn || (ele as any).fn?.fn === fn)) : [] effectMap.set(key, value) } } } export function emit( storage: object, key: string, value: any, oldValue: any, property?: string, ) { if (!hasChanged(value, oldValue)) return trigger(storage, key, value, oldValue) postMessage(storage, key, value, oldValue, property) } export function trigger( storage: object, key: string, value: any, oldValue: any, ) { const effectMap: EffectMap | undefined = storageEffectMap.get(storage) if (effectMap) { const effects: Effect[] | undefined = effectMap.get(key) if (effects) effects.forEach(ele => ele.fn.call(ele.ctx, value, oldValue)) } } ================================================ FILE: src/index.ts ================================================ export { createProxyStorage } from '@/proxy/storage' export * from '@/types' ================================================ FILE: src/proxy/broadcast.ts ================================================ import type { StorageLike } from '@/types' import { trigger } from '@/extends/watch' import { decode, simpleDecode, simpleEncode } from '@/proxy/transform' import { storageNameMap } from '@/shared' import { pThen } from '@/utils' const storageChannelMap = new Map() export function postMessage(storage: object, key: string, value: any, oldValue: any, property?: string) { const storageName = storageNameMap.get(storage) if (!storageName) return const channel = storageChannelMap.get(storageName) channel?.postMessage({ key, newValue: simpleEncode(value), oldValue: simpleEncode(oldValue), property, }) } export function listenMessage(storage: StorageLike) { const storageName = storageNameMap.get(storage)! let channel = storageChannelMap.get(storageName) if (!channel) storageChannelMap.set(storageName, (channel = new BroadcastChannel(`stokado:${storageName}`))) channel.onmessage = function (ev: MessageEvent) { const { key, newValue, oldValue, property } = ev.data trigger(storage, key, simpleDecode(newValue), simpleDecode(oldValue)) // update proxyStorage if (property) { pThen(() => storage.getItem(property), (res: string | null) => { return decode({ data: res, storage, property }) }) } } } ================================================ FILE: src/proxy/object.ts ================================================ import { getOptions } from '@/extends/options' import { emit } from '@/extends/watch' import { proxyObjectMap } from '@/shared' import { hasChanged, hasOwn, isArray, isIntegerKey, pThen } from '@/utils' import { encode } from './transform' const targetStorageMap = new WeakMap() function selfEmit( target: object, key: string, value: any, oldValue: any, ) { const { storage, storageProp } = targetStorageMap.get(target) const isIntKey = isArray(target) && isIntegerKey(key) const actualKey = isIntKey ? `${storageProp}[${key}]` : `${storageProp}.${key}` emit(storage, actualKey, value, oldValue, key !== 'length' ? storageProp : '') } function setStorageValue(target: object) { const { storage, storageProp } = targetStorageMap.get(target) // Check if the property still exists (not disposed) return pThen(() => storage.getItem(storageProp), (currentValue: string | null) => { if (currentValue === null) { // Property has been disposed, don't re-save it return } const encodeValue = encode({ data: target, storage, property: storageProp, options: getOptions(storage, storageProp) }) storage.setItem(storageProp, encodeValue) }) } let calling = false function createInstrumentations() { const instrumentations: Record = {}; // instrument length-altering mutation methods to track length (['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key: any) => { instrumentations[key] = function (target: Array) { return function (...args: any[]) { calling = true const oldLength: number = target.length const res = target[key](...args) setStorageValue(target) if (target.length !== oldLength) selfEmit(target, 'length', target.length, oldLength) calling = false return res } } }) return instrumentations } const arrayInstrumentations: Record = createInstrumentations() function get( target: object, key: string, receiver: any, ) { if (isArray(target) && hasOwn(arrayInstrumentations, key)) return arrayInstrumentations[key](target) return Reflect.get(target, key, receiver) } /** * array = []; * array[0] = 0; * array.push(1); * array.length = 3; */ function set( target: Record, key: string, value: any, receiver: object, ) { const arrayLength: number | undefined = isArray(target) ? target.length : undefined const oldValue = target[key] const hadKey = (isArray(target) && isIntegerKey(key)) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) if (result) { if (hasChanged(value, oldValue)) { // track `array.length = 3` length if (hadKey) selfEmit(target, key, value, oldValue) else selfEmit(target, key, value, undefined) } if (!calling) { // track `array[0] = 0` length if (key !== 'length' && arrayLength !== undefined && target.length !== arrayLength) selfEmit(target, 'length', target.length, arrayLength) setStorageValue(target) } } return result } function deleteProperty( target: Record, key: string, ) { const hadKey = hasOwn(target, key) const oldValue = target[key] const result = Reflect.deleteProperty(target, key) if (result && hadKey) { selfEmit(target, key, undefined, oldValue) setStorageValue(target) } return result } export function createProxyObject( target: object, storage?: Record, property?: string, ) { if (!storage && !property) return target const proxy = new Proxy(target, { get, set, deleteProperty, }) proxyObjectMap.set(proxy, target) targetStorageMap.set(target, { storage, storageProp: property, }) return proxy } ================================================ FILE: src/proxy/storage.ts ================================================ import type { StorageLike, StorageObject, StorageOptions } from '@/types' import { checkDisposable, setDisposable } from '@/extends/disposable' import { getExpires, removeExpires, setExpires } from '@/extends/expires' import { getOptions } from '@/extends/options' import { emit, off, on, once } from '@/extends/watch' import { listenMessage } from '@/proxy/broadcast' import { encode } from '@/proxy/transform' import { clearProxyStorage, deleteProxyStorageProperty, getProxyStorageProperty, getRaw, setProxyStorage, storageNameMap } from '@/shared' import { hasOwn, isArray, isFunction, isLocalStorage, isObject, isStorage, isString, pThen } from '@/utils' function clear(storage: Record) { return function () { clearProxyStorage(storage) } } function getItem(storage: Record) { return function (key: string) { return pThen(() => getProxyStorageProperty(storage, key), (res: StorageObject | string | null) => { const returnData = checkDisposable({ data: res, storage, property: key }) return isObject(returnData) ? returnData.value : returnData }) } } function removeItem(storage: Record) { return function (key: string) { deleteProxyStorageProperty(storage, key) } } function setItem( storage: Record, ) { return function (property: string, value: any, options?: StorageOptions) { return pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { const oldValue = isObject(res) ? res.value : (res || undefined) const oldOptions = isObject(res) ? res.options : {} const encodeValue = encode({ data: value, storage, property, options: Object.assign({}, oldOptions, options) }) storage.setItem(property, encodeValue) emit(storage, property, value, getRaw(oldValue), property) if (isArray(value) && isArray(oldValue)) emit(storage, `${property}.length`, value.length, oldValue.length) return true }) } } const instrumentations: Record = createInstrumentations() function createInstrumentations() { const nativeMethods: Record = { clear, getItem, setItem, removeItem, } const methods: Record = Object.assign({}, nativeMethods) const extendMethods: Record = { getExpires, getOptions, off, on, once, removeExpires, setDisposable, setExpires, } for (const methodName in extendMethods) { methods[methodName] = function (storage: Record) { return function (...args: any[]) { return extendMethods[methodName](storage, ...args) } } } return methods } function get( storage: Record, property: string, ) { if (hasOwn(instrumentations, property)) return instrumentations[property](storage) const data = storage[property] if (!isString(data) && data !== undefined) return isFunction(data) ? data.bind(storage) : data // priority: storage.getItem(property) > storage[property] return pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { const returnData = checkDisposable({ data: res, storage, property }) return isObject(returnData) ? returnData.value : data }) } function set( storage: Record, property: string, value: any, ) { return setItem(storage)(property, value) } function deleteProperty( storage: Record, property: string, ) { pThen(() => getProxyStorageProperty(storage, property), (res: StorageObject | string | null) => { deleteProxyStorageProperty(storage, property) const oldValue = isObject(res) ? res.value : (res || undefined) emit(storage, property, undefined, getRaw(oldValue), property) }) return true } export function createProxyStorage(storage: StorageLike, name?: string) { if (!isStorage(storage)) throw new Error('The parameter should be StorageLike object') const proxy = new Proxy(storage, { get, set, deleteProperty, }) setProxyStorage(storage, {}) if (!name) console.warn('If you are using IndexedDB or WebSQL, `name` is required.') if (name || isLocalStorage(storage)) { storageNameMap.set(storage, name || 'localStorage') listenMessage(storage) } return proxy } ================================================ FILE: src/proxy/transform.ts ================================================ import type { RawType, StorageObject, StorageOptions } from '@/types' import { createProxyObject } from '@/proxy/object' import { setProxyStorageProperty } from '@/shared' import { getRawType, isObject, isString, transformEval, transformJSON } from '@/utils' interface Serializer { read: (raw: any, storage?: Record, property?: string) => T write: (value: T) => any } const identity = (v: T): T => v const toString = (v: any): string => String(v) const StorageSerializers: Record> = { String: { read: identity, write: identity, }, Number: { read: Number.parseFloat, write: toString, }, BigInt: { read: BigInt, write: toString, }, Boolean: { read: (v: string) => v === 'true', write: toString, }, Null: { read: () => null, write: () => 'null', }, Undefined: { read: () => undefined, write: () => 'undefined', }, Object: { read: (v, storage, property) => createProxyObject(v, storage, property), write: identity, }, Array: { read: (v, storage, property) => createProxyObject(v, storage, property), write: identity, }, Set: { read: (v: any[]) => new Set(v), write: (v: Set) => [...v], }, Map: { read: (v: [any, any][]) => new Map(v), write: (v: Map) => [...v], }, Date: { read: (v: string) => new Date(v), write: toString, }, URL: { read: (v: string) => new URL(v), write: toString, }, RegExp: { read: (v: string) => transformEval(v), write: toString, }, Function: { read: (v: string) => transformEval(`(function() { return ${v} })()`), write: toString, }, } export function decode({ data, storage, property, }: { data: string | null storage?: Record property?: string }): any { if (!isString(data)) return data const nativeData: object | string = transformJSON(data) if (!isObject(nativeData)) return nativeData const serializer = StorageSerializers[nativeData?.type as RawType] if (!serializer) return nativeData nativeData.value = ['Object', 'Array'].includes(nativeData.type) ? serializer.read(nativeData.value, storage, property) : serializer.read(nativeData.value) if (storage && property) setProxyStorageProperty(storage, property, nativeData as StorageObject) return nativeData } export function encode({ data, storage, property, options, }: { data: any storage?: Record property?: string options?: StorageOptions }) { const rawType = getRawType(data) const serializer = StorageSerializers[rawType] if (!serializer) throw new Error(`can't set "${rawType}" property.`) const storageObject: StorageObject = { type: rawType, value: serializer.write(data), options, } if (storage && property) { setProxyStorageProperty(storage, property, { type: rawType, value: ['Object', 'Array'].includes(rawType) ? serializer.read(storageObject.value, storage, property) : serializer.read(storageObject.value), options, }) } return JSON.stringify(storageObject) } export function simpleDecode(data: string) { const nativeData: { type: string, value: string | object } = transformJSON(data) as { type: string, value: string | object } const serializer = StorageSerializers[nativeData.type as RawType] return serializer.read(nativeData.value) } export function simpleEncode(data: any) { const rawType = getRawType(data) const serializer = StorageSerializers[rawType] return JSON.stringify({ type: rawType, value: serializer.write(data), }) } ================================================ FILE: src/shared.ts ================================================ import type { StorageObject } from '@/types' import { decode } from '@/proxy/transform' import { pThen } from '@/utils' import { checkExpired } from './extends/expires' const proxyStorageMap = new WeakMap, Record>() export const storageNameMap = new WeakMap, string>() export function setProxyStorage(storage: Record, proxy: Record): void { proxyStorageMap.set(storage, proxy) } export function clearProxyStorage(storage: Record): void { storage.clear() proxyStorageMap.set(storage, {}) } export function getProxyStorageProperty(storage: Record, property: string): StorageObject | string | null { const proxyStorage = proxyStorageMap.get(storage) const data = proxyStorage![property] || pThen(() => storage.getItem(property), (res: string | null) => { return decode({ data: res, storage, property }) }) return pThen(() => data, (res: StorageObject | string | null) => { return checkExpired({ data: res, storage, property }) }) } export function deleteProxyStorageProperty(storage: Record, property: string) { const proxyStorage = proxyStorageMap.get(storage) storage.removeItem(property) delete proxyStorage![property] } export function setProxyStorageProperty(storage: Record, property: string, data: StorageObject) { const proxyStorage = proxyStorageMap.get(storage) proxyStorage![property] = data } export const proxyObjectMap = new WeakMap, Record>() export function getRaw(value: any) { return proxyObjectMap.get(value) || value } ================================================ FILE: src/types.ts ================================================ export type RawType = 'String' | 'Number' | 'BigInt' | 'Boolean' | 'Null' | 'Undefined' | 'Object' | 'Array' | 'Set' | 'Map' | 'Date' | 'RegExp' | 'URL' | 'Function' export interface StorageLike { [x: string]: any clear: () => void getItem: (key: string) => string | null | Promise key: (key: number) => string | null | Promise setItem: (key: string, value: any, options?: StorageOptions) => void removeItem: (key: string) => void length: number } export type StorageValue = string | number | bigint | boolean | null | undefined | object export interface StorageOptions { expires?: ExpiresType disposable?: boolean } export type EffectMap = Map export type EffectFn = ( value?: V, oldValue?: OV ) => any export interface Effect { ctx: any fn: EffectFn } export interface StorageObject { type: string value: any options?: StorageOptions } export type ExpiresType = string | number | Date ================================================ FILE: src/utils.ts ================================================ import type { RawType, StorageLike } from '@/types' export const isArray = Array.isArray export function isSet(val: unknown): val is Set { return getTypeString(val) === '[object Set]' } export function isMap(val: unknown): val is Map { return getTypeString(val) === '[object Map]' } export function isDate(val: unknown): val is Date { return getTypeString(val) === '[object Date]' } export function isRegExp(val: unknown): val is RegExp { return getTypeString(val) === '[object RegExp]' } export function isURL(val: unknown): val is URL { return getTypeString(val) === '[object URL]' } export function isError(val: unknown): val is Error { return !!val && Object.getPrototypeOf(val)?.name === 'Error' } export function isFunction(val: unknown): val is Function { return typeof val === 'function' } export function isNumber(val: unknown): val is number { return typeof val === 'number' } export function isString(val: unknown): val is string { return typeof val === 'string' } export function isObject(val: unknown): val is Record { return val !== null && typeof val === 'object' } export function isPromise(val: unknown): val is Promise { return ( (isObject(val) || isFunction(val)) && isFunction((val as any).then) && isFunction((val as any).catch) ) } export function isIntegerKey(key: unknown) { return typeof key === 'string' && key !== 'NaN' && key[0] !== '-' && `${Number.parseInt(key, 10)}` === key } export async function isStorage(storage: StorageLike) { return ['clear', 'getItem', 'key', 'setItem', 'removeItem'].every(method => isFunction(storage[method])) } export function isLocalStorage(storage: StorageLike) { return storage === window.localStorage } export function isSessionStorage(storage: StorageLike) { return storage === window.sessionStorage } export function getTypeString(value: unknown): string { return Object.prototype.toString.call(value) } export function getRawType(value: unknown): RawType { return getTypeString(value).slice(8, -1) as RawType } export function hasChanged(value: any, oldValue: any): boolean { return !Object.is(value, oldValue) } export function transformJSON( data: string, ): object | string { try { return JSON.parse(data) } catch { return data } } // prototies exist in the prototype chain export function propertyIsInPrototype(object: object, prototypeName: string) { return !hasOwn(object, prototypeName) && (prototypeName in object) } const hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwn(val: object, key: string | symbol): key is keyof typeof val { return hasOwnProperty.call(val, key) } export function transformEval(code: string) { // runs in the global scope rather than the local one // eslint-disable-next-line no-eval const eval2 = eval return (function () { return eval2(code) })() } export function formatTime(time: any) { if (isDate(time)) return time.getTime() if (isString(time)) return +time.padEnd(13, '0') return time } let prevPromise = Promise.resolve() // TODO: Maybe there is a better solution export function pThen(getter: Function, callback: Function) { const maybePromise = getter() if (isPromise(maybePromise)) { prevPromise = prevPromise.then(() => getter()).then((res) => { prevPromise = Promise.resolve() return callback(res) }) return prevPromise } else { return callback(maybePromise) } } ================================================ FILE: tests/base.spec.ts ================================================ import { expect, test } from '@playwright/test' import { decode, encode } from '@/proxy/transform' import './global.d.ts' test.describe('basic usage', () => { test('transform', async ({ page }) => { await page.goto('/') const proxyStorage = await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' return localStorage.test }) expect(proxyStorage).toBe(encode({ data: 'hello stokado', options: {} })) expect(decode({ data: proxyStorage }).value).toBe('hello stokado') }) test('set, read and delete', async ({ page }) => { await page.goto('/') // set await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' }) // read expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello stokado') // delete expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) delete local.test return local.test })).toBeUndefined() }) test('localStorage methods', async ({ page }) => { await page.goto('/') // key() setItem() expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello localStorage' local.setItem('foo', 'bar') local.setItem('test', 'hello stokado') // The order of keys is user-agent defined return local.key(0) })).toBe('test') // getItem() expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.getItem('test') })).toBe('hello stokado') // length expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.length })).toBe(2) // removeItem() expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.removeItem('test') return local.test })).toBeUndefined() // clear() expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.clear() return local.length })).toEqual(0) }) }) ================================================ FILE: tests/disposable.spec.ts ================================================ import { expect, test } from '@playwright/test' import './global.d.ts' test.describe('disposable', () => { test('setDisposable', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setDisposable('test') }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello stokado') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('setDisposable width set', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setDisposable('test') }) await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello world' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello world') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('setDisposable width object', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = { hello: 'world' } local.setDisposable('test') return local.test.hello })).toBe('world') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = { hello: 'world' } local.setDisposable('test') local.test.hello = 'stokado' return local.test })).toBeUndefined() expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = { hello: 'world' } local.setDisposable('test') delete local.test.hello return local.test })).toBeUndefined() }) }) ================================================ FILE: tests/equal.spec.ts ================================================ /* eslint-disable no-self-compare */ import { expect, test } from '@playwright/test' import './global.d.ts' test.describe('equal object', () => { test('Standard built-in objects', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = { $string: 'hello stokado', $number: 0, $boolean: true, $null: null, $undefined: undefined, } return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = [] local.test[0] = 'hello' local.test.push('stokado') return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Date() return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new RegExp('ab+c') return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new URL(location.href) return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) function foo() { return 'hello stokado!' } local.test = foo return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Set(['hello stokado']) return local.test === local.test })).toBe(true) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Map([['hello', 'stokado'], ['foo', 'bar']]) return local.test === local.test })).toBe(true) }) }) ================================================ FILE: tests/expired.spec.ts ================================================ import { expect, test } from '@playwright/test' import './global.d.ts' async function delay(ms?: number) { return new Promise(resolve => setTimeout(resolve, ms)) } test.describe('expired', () => { test('setExpires', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setExpires('test', Date.now() + 1000) return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('getExpires', async ({ page }) => { await page.goto('/') const expires = Date.now() + 1000 await page.evaluate((expires) => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setExpires('test', expires) return local.test }, expires) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.getExpires('test') })).toEqual(new Date(expires)) }) test('removeExpires', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setExpires('test', Date.now() + 1000) }) await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.removeExpires('test') }) delay(1200) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toEqual('hello stokado') }) test('setExpires with object', async ({ page }) => { await page.goto('/') const expires = Date.now() + 1000 expect(await page.evaluate((expires) => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = { hello: 'world' } local.setExpires('test', expires) local.test.hello = 'stokado' local.test.other = 'stokado' return local.test }, expires)).toEqual({ hello: 'stokado', other: 'stokado' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.getExpires('test') })).toEqual(new Date(expires)) await delay(1000) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) }) ================================================ FILE: tests/global.d.ts ================================================ import type { StorageLike } from '@/types' declare global { interface Window { stokado: { createProxyStorage: (storage: T, name?: string) => T } localforage: StorageLike & { length: (callback?: (err: any, numberOfKeys: number) => void) => Promise } } } ================================================ FILE: tests/localforage.spec.ts ================================================ import { expect, test } from '@playwright/test' import './global.d.ts' async function delay(ms?: number) { return new Promise(resolve => setTimeout(resolve, ms)) } test.describe('localforage', () => { test('base', async ({ page }) => { await page.goto('/') // set await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) await (local.test = 'hello stokado') }) // get expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.test })).toBe('hello stokado') // delete expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) delete local.test return await local.test })).toBeUndefined() }) test('methods', async ({ page }) => { await page.goto('/') // key() setItem() expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) await (local.test = 'hello localforage') await local.setItem('test', 'hello stokado') await local.setItem('foo', 'bar') return await local.key(0) })).toBe('foo') // getItem() expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.getItem('test') })).toBe('hello stokado') // length expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.length() })).toBe(2) // removeItem() expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) local.removeItem('test') return await local.test })).toBeUndefined() // clear() expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) local.clear() return await local.length() })).toEqual(0) }) test('subscribe', async ({ context }) => { const page1 = await context.newPage() await page1.goto('/') expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage, 'localforage') return new Promise((resolve) => { local.on('test.length', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) local.test = ['hello', 'stokado']; (local.test).then((array: Array) => array.pop()) }) })).toEqual({ newVal: 1, oldVal: 2, }) // another tab const page2 = await context.newPage() await page2.goto('/') setTimeout(() => { page2.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage, 'localforage') local.test = [] }) }) expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage, 'localforage') return new Promise((resolve) => { local.on('test.length', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) }) })).toEqual({ newVal: 0, oldVal: 1, }) }) test('expired', async ({ page }) => { await page.goto('/') expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) local.test = 'hello stokado' local.setExpires('test', Date.now() + 1000) return await local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.test })).toBeUndefined() }) test('disposable', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) local.test = 'hello stokado' local.setDisposable('test') }) expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.test })).toBe('hello stokado') expect(await page.evaluate(async () => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return await local.test })).toBeUndefined() }) test('setOptions', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) local.setItem('test', 'hello stokado', { expires: Date.now() + 1000, }) return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(window.localforage) return local.test })).toBeUndefined() }) }) ================================================ FILE: tests/serializer.spec.ts ================================================ import { expect, test } from '@playwright/test' import { decode } from '@/proxy/transform' import './global.d.ts' test.describe('serialized value', () => { test('number', async ({ page }) => { await page.goto('/') // 1 expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 1 return local.test })).toBe(1) // 0 expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 0 return local.test })).toBe(0) // -1 expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = -1 return local.test })).toBe(-1) // 2.71 expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 2.71 return local.test })).toBe(2.71) // NaN expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = Number.NaN return local.test })).toBeNaN() // Infinity expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = Infinity return local.test })).toBe(Infinity) // -Infinity expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = -Infinity return local.test })).toBe(-Infinity) // new Number expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Number(3.14) return local.test })).toBe(3.14) }) test('bigint', async ({ page }) => { await page.goto('/') // 1n expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 1n return local.test })).toBe(1n) }) test('boolean', async ({ page }) => { await page.goto('/') // true expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = true return local.test })).toBe(true) // false expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = false return local.test })).toBe(false) // new Boolean expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Boolean(false) return local.test })).toBe(false) }) test('undefined', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = undefined return local.test })).toBeUndefined() }) test('null', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = null return local.test })).toBeNull() }) test('Object', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) // JSON.stringify don't know how to serialize a BigInt local.test = { $string: 'hello stokado', $number: 0, $boolean: true, $null: null, $undefined: undefined, } return local.test })).toEqual({ $string: 'hello stokado', $number: 0, $boolean: true, $null: null, $undefined: undefined, }) }) test('Array', async ({ page }) => { await page.goto('/') // [] expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = [] return local.test })).toEqual([]) // ['hello'] expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test[0] = 'hello' return local.test })).toEqual(['hello']) // length expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test.length = 0 return local.test })).toEqual([]) // push expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test.push('hello', 'stokado') return local.test })).toEqual(['hello', 'stokado']) // pop expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test.pop() })).toBe('stokado') }) test('Date', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Date('2000-01-01T00:00:00.000Z') return local.test })).toEqual(new Date('2000-01-01T00:00:00.000Z')) }) test('URL', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new URL('https://github.com/') return local.test })).toEqual(new URL('https://github.com/')) }) test('RegExp', async ({ page }) => { await page.goto('/') // new RegExp expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new RegExp('ab+c') return local.test })).toEqual(new RegExp('ab+c')) // Literal expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = /ab+c/ return local.test })).toEqual(/ab+c/) }) test('Function', async ({ page }) => { await page.goto('/') // Function declaration expect(decode({ data: await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) function foo() { return 'hello stokado!' } local.test = foo return localStorage.test }), }).value()).toBe('hello stokado!') // Function expression expect(decode({ data: await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = function () { return 'hello stokado!' } return localStorage.test }), }).value()).toBe('hello stokado!') // Arrow function expect(decode({ data: await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = () => { return 'hello stokado!' } return localStorage.test }), }).value()).toBe('hello stokado!') }) test('Set', async ({ page }) => { await page.goto('/') expect(decode({ data: await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Set(['hello stokado']) return localStorage.test }), }).value).toEqual(new Set(['hello stokado'])) }) test('Map', async ({ page }) => { await page.goto('/') expect(decode({ data: await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = new Map([['hello', 'stokado'], ['foo', 'bar']]) return localStorage.test }), }).value).toEqual(new Map([['hello', 'stokado'], ['foo', 'bar']])) }) }) ================================================ FILE: tests/storage.spec.ts ================================================ import { expect, test } from '@playwright/test' import './global.d.ts' test.describe('storage', () => { test('local first', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) const session = createProxyStorage(sessionStorage) local.test = 'hello local' session.test = 'hello session' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello local') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const session = createProxyStorage(sessionStorage) return session.test })).toBe('hello session') }) test('session first', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const session = createProxyStorage(sessionStorage) const local = createProxyStorage(localStorage) session.test = 'hello session' local.test = 'hello local' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const session = createProxyStorage(sessionStorage) return session.test })).toBe('hello session') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello local') }) test('local disposable and session', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) const session = createProxyStorage(sessionStorage) local.test = 'hello local' local.setDisposable('test') session.test = 'hello session' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello local') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const session = createProxyStorage(sessionStorage) return session.test })).toBe('hello session') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('local only', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello local' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const session = createProxyStorage(sessionStorage) return session.test })).toBeUndefined() }) test('session only', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const session = createProxyStorage(sessionStorage) session.test = 'hello local' }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) }) ================================================ FILE: tests/storageOptions.spec.ts ================================================ import { expect, test } from '@playwright/test' import './global.d.ts' async function delay(ms?: number) { return new Promise(resolve => setTimeout(resolve, ms)) } test.describe('setItem', async () => { test('expired', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setItem('test', 'hello stokado', { expires: Date.now() + 1000, }) return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('expired after disposable', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setItem('test', 'hello stokado', { expires: Date.now() + 1000, }) return local.test })).toBe('hello stokado') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setDisposable('test') return local.test })).toBe('hello stokado') await delay(500) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('disposable after expired', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setItem('test', 'hello stokado', { expires: Date.now() + 1000, }) return local.test })).toBe('hello stokado') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setDisposable('test') }) await delay(1000) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('disposable after removeExpires', async ({ page }) => { await page.goto('/') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setItem('test', 'hello stokado', { expires: Date.now() + 1000, }) return local.test })).toBe('hello stokado') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setDisposable('test') }) await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.removeExpires('test') }) await delay(1000) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBe('hello stokado') expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.test })).toBeUndefined() }) test('getOptions', async ({ page }) => { await page.goto('/') const options = { expires: Date.now() + 1000, disposable: true, } await page.evaluate((options) => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.setItem('test', 'hello stokado', options) }, options) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.getOptions('test') })).toEqual(options) }) test('getOptions:expired', async ({ page }) => { await page.goto('/') const expires = Date.now() + 1000 await page.evaluate((expires) => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setExpires('test', expires) }, expires) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.getOptions('test') })).toEqual({ expires }) }) test('getOptions:disposable', async ({ page }) => { await page.goto('/') await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = 'hello stokado' local.setDisposable('test') }) expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.getOptions('test') })).toEqual({ disposable: true }) }) }) ================================================ FILE: tests/subscribe.spec.ts ================================================ import { expect, test } from '@playwright/test' import './global.d.ts' test.describe('subscribe', async () => { test('on Object', async ({ context }) => { const page1 = await context.newPage() await page1.goto('/') // First-level object expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) setTimeout(() => { local.test = {} }) return new Promise((resolve) => { local.on('test', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) }) })).toEqual({ newVal: {}, oldVal: undefined, }) // Second-level object expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) setTimeout(() => { local.test = {} local.test.foo = 'bar' }) return new Promise((resolve) => { local.on('test.foo', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) }) })).toEqual({ newVal: 'bar', oldVal: undefined }) // two page const page2 = await context.newPage() await page2.goto('/') // clear page2.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.clear() }) setTimeout(() => { page2.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = {} }) }) expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return new Promise((resolve) => { local.on('test', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) }) })).toEqual({ newVal: {}, oldVal: undefined, }) }) test('on Array', async ({ context }) => { const page1 = await context.newPage() await page1.goto('/') expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) setTimeout(() => { local.test = ['hello', 'stokado'] local.test.pop() }) return new Promise((resolve) => { local.on('test.length', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) }) })).toEqual({ newVal: 1, oldVal: 2, }) // another tab const page2 = await context.newPage() await page2.goto('/') setTimeout(() => { page2.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = [] }) }) expect(await page1.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return new Promise((resolve) => { local.on('test.length', (newVal: any, oldVal: any) => { resolve({ newVal, oldVal, }) }) }) })).toEqual({ newVal: 0, oldVal: 1, }) }) test('once', async ({ page }) => { await page.goto('/') // once await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.count = 0 local.once('test', () => { local.count++ }) }) // trigger await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = {} local.test = [] }) // count expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.count })).toBe(1) }) test('off', async ({ page }) => { await page.goto('/') // on await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.count = 0 local.on('test', () => { local.count++ }) }) // trigger and off await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) local.test = {} local.test = [] local.off('test') local.test = 1 local.test = true }) // count expect(await page.evaluate(() => { const { createProxyStorage } = window.stokado const local = createProxyStorage(localStorage) return local.count })).toBe(2) }) }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "lib": ["DOM", "ESNEXT"], "baseUrl": "./src", "rootDir": ".", "module": "ESNext", "moduleResolution": "Bundler", "paths": { "@/*": ["*"] }, "resolveJsonModule": true, "strict": true, "strictNullChecks": true, "noEmit": true, "esModuleInterop": true, "verbatimModuleSyntax": true, "skipDefaultLibCheck": true, "skipLibCheck": true }, "include": ["src/*.ts", "src/*/*.ts", "tests/*.ts"] } ================================================ FILE: v2.md ================================================ ```shell __ __ __ __ ____ /\ \__ ___ /\ \/ \ __ /\ \ ___ / ,__\ \ \ ,_\ / __`\ \ \ < /'__`\ \_\ \ / __`\ /\__, `\ \ \ \/ /\ \_\ \ \ \ ^ \ /\ \_\.\_ /\ ,. \ /\ \_\ \ \/\____/ \ \ \_ \ \____/ \ \_\ \_\\ \__/.\_\\ \____\\ \____/ \/___/ \ \__\ \/___/ \/_/\/_/ \/__/\/_/ \/___ / \/___/ \/__/ ``` **English | [中文](./v2.zh.md)** *Stokado*(/stəˈkɑːdoʊ/) is the [Esperanto](https://en.wikipedia.org/wiki/Esperanto)(an international auxiliary language) for *storage*, meaning that *Stokado* is also an auxiliary agent for *storage*. *Stokado* uses `proxy` to better and more conveniently manage *storage*, enabling features such as syntax sugar, serialization, event listeners, expiration settings, and one-time value. try it on [codesandbox](https://codesandbox.io/s/proxy-web-storage-demo-3w6uex), or check out the test cases in the **tests** folder. ### Install ```shell npm i stokado ``` ```js // mjs import { local, session } from 'stokado' ``` ```js // cjs const { local, session } = require('stokado') ``` ### CDN ```html ``` ### Features #### 1. Syntax sugar Keep the type of storage value unchanged and change array and object directly. ```js import { local, session } from 'stokado' local.test = 'hello stokado' // works delete local.test // works // number local.test = 0 local.test === 0 // true // boolean local.test = false local.test === false // true // undefined local.test = undefined local.test === undefined // true // null local.test = null local.test === null // true // object local.test = { hello: 'world' } local.test.hello = 'stokado' // works // array local.test = ['hello'] local.test.push('stokado') // works local.test.length // 2 // Date local.test = new Date('2000-01-01T00:00:00.000Z') local.test.getTime() === 946684800000 // true // RegExp local.test = /d(b+)d/g local.test.test('cdbbdbsbz') // true // function local.test = function () { return 'hello stokado!' } local.test() === 'hello stokado!' // true ``` `test` is the key in localStorage. The value is also saved to localStorage. The `local`, `session` also have the same methods and properties: `key()`, `getItem()`, `setItem()`, `removeItem()`, `clear()` and `length`. **Extra:** `setItem(key, value, options)` supports setting attributes, `options` configuration fields are as follows: | | type | effect | | ---- | ---- | ---- | | expires | string \| number \| Date | set the expires for the item | | disposable | boolean | set a one-time value for the item | #### 2. Subscribe Listen to the changes. ```js import { local } from 'stokado' local.on('test', (newVal, oldVal) => { console.log('test', newVal, oldVal) }) local.on('test.a', (newVal, oldVal) => { console.log('test.a', newVal, oldVal) }) local.test = {} // test {} undefined local.test.a = 1 // test.a 1 undefined ``` ##### on Subscribe to an item. - `key`: the name of the item to subscribe to. Support `obj.a` for `Object` and `list[0]` for `Array`, and also `Array` length. - `callback`: the function to call when the item is changed. Includes `newValue` and `oldValue`. ##### once Subscribe to an item only once. - `key`: the name of the item to subscribe to. Support `obj.a` for `Object` and `list[0]` for `Array`. - `callback`: the function to call when the item is changed. Includes `newValue` and `oldValue`. ##### off Unsubscribe from an item or all items. - `key(optional)`: the name of the item to unsubscribe from. If no key is provided, it unsubscribes you from all items. - `callback(optional)`: the function used when binding to the item. If no callback is provided, it unsubscribes you from all functions binding to the item. #### 3. Expired Set expires for items. ```js import { local } from 'stokado' local.setItem('test', 'hello stokado', { expires: Date.now() + 10000 }) // local.test = 'hello stokado' // local.setExpires('test', Date.now() + 10000) // within 10's local.test // 'hello stokado' // after 10's local.test // undefined ``` The expires is saved to localStorage. So no matter how you reload it within 10's, the value still exists. But after 10's, it has been removed. ##### setExpires Set expires for an item. - `key`: the name of the item to set expires. - `expires`: accept `string`、`number` and `Date`. ##### getExpires Return the expires(`Date`) of the item. - `key`: the name of the item that has set expires. ##### removeExpires Cancel the expires of the item. - `key`: the name of the item that has set expires. #### 4. Disposable Get the value once. ```js import { local } from 'stokado' local.setItem('test', 'hello stokado', { disposable: true }) // local.test = 'hello stokado' // local.setDisposable('test') local.test // 'hello stokado' local.test // undefined ``` ##### setDisposable Set a one-time value for the item. - `key`:the name of the item to set disposable. ================================================ FILE: v2.zh.md ================================================ ```shell __ __ __ __ ____ /\ \__ ___ /\ \/ \ __ /\ \ ___ / ,__\ \ \ ,_\ / __`\ \ \ < /'__`\ \_\ \ / __`\ /\__, `\ \ \ \/ /\ \_\ \ \ \ ^ \ /\ \_\.\_ /\ ,. \ /\ \_\ \ \/\____/ \ \ \_ \ \____/ \ \_\ \_\\ \__/.\_\\ \____\\ \____/ \/___/ \ \__\ \/___/ \/_/\/_/ \/__/\/_/ \/___ / \/___/ \/__/ ``` **[English](./v2.md) | 中文** *stokado*(/stəˈkɑːdoʊ/) 是 *storage* 的[世界语](https://zh.wikipedia.org/wiki/%E4%B8%96%E7%95%8C%E8%AF%AD)(一种国际辅助语言),喻意为 *stokado* 也是 *storage* 的辅助代理。 *stokado* 借助 `proxy`,更好地更方便地管理 *storage*,实现了相关语法糖、序列化、监听订阅、设置过期、一次性取值等功能。 在[codesandbox](https://codesandbox.io/s/proxy-web-storage-demo-3w6uex)试一试,也可以查看 **tests** 文件夹下的测试用例。 ### Install ```shell npm i stokado ``` ```js // mjs import { local, session } from 'stokado' ``` ```js // cjs const { local, session } = require('stokado') ``` ### CDN ```html ``` ### Features #### 1. Syntax sugar 保持`storage`值的类型不变并且可以直接操作数组和对象。 ```js import { local, session } from 'stokado' local.test = 'hello stokado' // works delete local.test // works // number local.test = 0 local.test === 0 // true // boolean local.test = false local.test === false // true // undefined local.test = undefined local.test === undefined // true // null local.test = null local.test === null // true // object local.test = { hello: 'world' } local.test.hello = 'stokado' // works // array local.test = ['hello'] local.test.push('stokado') // works local.test.length // 2 // Date local.test = new Date('2000-01-01T00:00:00.000Z') local.test.getTime() === 946684800000 // true // RegExp local.test = /d(b+)d/g local.test.test('cdbbdbsbz') // true // function local.test = function () { return 'hello stokado!' } local.test() === 'hello stokado!' // true ``` `test`和对应的`value`是实际保存到`localStorage`的。同时,`local`和`session`也支持`Web Storage`的方法和属性:`key()`,`getItem()`,`setItem()`,`removeItem()`,`clear()` 和 `length`。 **Extra:** `setItem(key, value, options)` 支持设置属性,`options` 配置字段如下: | | 类型 | 作用 | | ---- | ---- | ---- | | expires | string \| number \| Date | 设置过期时间 | | disposable | boolean | 设置一次性 | #### 2. Subscribe 监听值的变化。 ```js import { local } from 'stokado' local.on('test', (newVal, oldVal) => { console.log('test', newVal, oldVal) }) local.on('test.a', (newVal, oldVal) => { console.log('test.a', newVal, oldVal) }) local.test = {} // test {} undefined local.test.a = 1 // test.a 1 undefined ``` ##### on 监听指定项。 参数: - `key`:监听指定项的名字。支持对象的二级监听,例如:`obj.a` 对于 `Object` 和 `list[0]` 对于 `Array`,还支持数组长度的监听。 - `callback`:指定项的值发生变化时,触发的回调函数。参数包括`newValue` 和 `oldValue`。 ##### once 只监听指定项一次。 - `key`:监听指定项的名字。支持对象的二级监听,例如:`obj.a` 对于 `Object` 和 `list[0]` 对于 `Array`,还支持数组长度的监听。 - `callback`:指定项的值发生变化时,触发的回调函数。参数包括`newValue` 和 `oldValue`。 ##### off 取消监听指定项或者移除所有监听。 - `key(可选)`:期望移除监听的指定项。如果为空,则移除所有监听。 - `callback(可选)`:移除指定项的某一回调函数。如果为空,则移除指定项绑定的所有监听事件。 #### 3. Expired 为指定项设置过期时间。 ```js import { local } from 'stokado' local.setItem('test', 'hello stokado', { expires: Date.now() + 10000 }) // local.test = 'hello stokado' // local.setExpires('test', Date.now() + 10000) // within 10's local.test // 'hello stokado' // after 10's local.test // undefined ``` 过期时间也会保存到`Web Storage`中,并不会刷新页面导致过期失效。 所以在10秒内无论你怎么刷新,值还是会存在。 但是在10秒以后,指定项就被移除了。 ##### setExpires 为指定项设置过期时间。 - `key`:设置过期的指定项名字。 - `expires`:过期时间。接受`string`、`number` 和 `Date`类型。 ##### getExpires 获取指定的过期时间,返回类型为`Date`。 - `key`: 设置了过期时间的指定项名字。 ##### removeExpires 取消指定项的过期设置。 - `key`: 设置了过期时间的指定项名字。 #### 4. Disposable 一次性取值。 ```js import { local } from 'stokado' local.setItem('test', 'hello stokado', { disposable: true }) // local.test = 'hello stokado' // local.setDisposable('test') local.test // 'hello stokado' local.test // undefined ``` ##### setDisposable 为指定项设置一次性取值。 - `key`:设置一次性的指定项名字。