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 `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>${options.title}</title>
</head>
<body>
<script src="${options.files.js[0].fileName}"></script>
<script src="https://cdn.jsdelivr.net/npm/localforage/dist/localforage.min.js"></script>
</body>
</html>
`
}
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<string, any>,
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<string, any>
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<string, any>,
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<string, any>,
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<string, any>,
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<string, any>
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<string, any>,
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<object, EffectMap>()
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<string, BroadcastChannel>()
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<string, Function> = {};
// instrument length-altering mutation methods to track length
(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key: any) => {
instrumentations[key] = function (target: Array<any>) {
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<string, Function> = 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<string, any>,
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<string, any>,
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<string, any>,
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<string, any>) {
return function () {
clearProxyStorage(storage)
}
}
function getItem(storage: Record<string, any>) {
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<string, any>) {
return function (key: string) {
deleteProxyStorageProperty(storage, key)
}
}
function setItem(
storage: Record<string, any>,
) {
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<string, Function> = createInstrumentations()
function createInstrumentations() {
const nativeMethods: Record<string, Function> = {
clear,
getItem,
setItem,
removeItem,
}
const methods: Record<string, Function> = Object.assign({}, nativeMethods)
const extendMethods: Record<string, Function> = {
getExpires,
getOptions,
off,
on,
once,
removeExpires,
setDisposable,
setExpires,
}
for (const methodName in extendMethods) {
methods[methodName] = function (storage: Record<string, any>) {
return function (...args: any[]) {
return extendMethods[methodName](storage, ...args)
}
}
}
return methods
}
function get(
storage: Record<string, any>,
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<string, any>,
property: string,
value: any,
) {
return setItem(storage)(property, value)
}
function deleteProperty(
storage: Record<string, any>,
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<T> {
read: (raw: any, storage?: Record<string, any>, property?: string) => T
write: (value: T) => any
}
const identity = <T>(v: T): T => v
const toString = (v: any): string => String(v)
const StorageSerializers: Record<string, Serializer<any>> = {
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<any>) => [...v],
},
Map: {
read: (v: [any, any][]) => new Map(v),
write: (v: Map<any, any>) => [...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<string, any>
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<string, any>
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<string, any>, Record<string, any>>()
export const storageNameMap = new WeakMap<Record<string, any>, string>()
export function setProxyStorage(storage: Record<string, any>, proxy: Record<string, any>): void {
proxyStorageMap.set(storage, proxy)
}
export function clearProxyStorage(storage: Record<string, any>): void {
storage.clear()
proxyStorageMap.set(storage, {})
}
export function getProxyStorageProperty(storage: Record<string, any>, 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<string, any>, property: string) {
const proxyStorage = proxyStorageMap.get(storage)
storage.removeItem(property)
delete proxyStorage![property]
}
export function setProxyStorageProperty(storage: Record<string, any>, property: string, data: StorageObject) {
const proxyStorage = proxyStorageMap.get(storage)
proxyStorage![property] = data
}
export const proxyObjectMap = new WeakMap<Record<string, any>, Record<string, any>>()
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<string | null>
key: (key: number) => string | null | Promise<string | null>
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<string, Effect[]>
export type EffectFn<V = any, OV = any> = (
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<any> {
return getTypeString(val) === '[object Set]'
}
export function isMap(val: unknown): val is Map<any, any> {
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<any, any> {
return val !== null && typeof val === 'object'
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
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<number> }
}
}
================================================
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<string>) => 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
<script src="https://cdn.jsdelivr.net/npm/stokado"></script>
<!-- or https://www.unpkg.com/stokado -->
<script>
const { local, session } = window.stokado
</script>
```
### 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
<script src="https://cdn.jsdelivr.net/npm/stokado"></script>
<!-- or https://www.unpkg.com/stokado -->
<script>
const { local, session } = window.stokado
</script>
```
### 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`:设置一次性的指定项名字。
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
SYMBOL INDEX (81 symbols across 18 files)
FILE: playground/template.js
function template (line 1) | function template(options) {
FILE: src/extends/disposable.ts
function setDisposable (line 6) | function setDisposable(
function checkDisposable (line 19) | function checkDisposable({
FILE: src/extends/expires.ts
function setExpires (line 7) | function setExpires(
function getExpires (line 28) | function getExpires(
function removeExpires (line 40) | function removeExpires(
function checkExpired (line 53) | function checkExpired({
FILE: src/extends/options.ts
function getOptions (line 5) | function getOptions(
FILE: src/extends/watch.ts
function on (line 7) | function on(
function once (line 29) | function once(
function off (line 44) | function off(
function emit (line 65) | function emit(
function trigger (line 79) | function trigger(
FILE: src/proxy/broadcast.ts
function postMessage (line 9) | function postMessage(storage: object, key: string, value: any, oldValue:...
function listenMessage (line 23) | function listenMessage(storage: StorageLike) {
FILE: src/proxy/object.ts
function selfEmit (line 9) | function selfEmit(
function setStorageValue (line 22) | function setStorageValue(target: object) {
function createInstrumentations (line 36) | function createInstrumentations() {
function get (line 61) | function get(
function set (line 78) | function set(
function deleteProperty (line 111) | function deleteProperty(
function createProxyObject (line 126) | function createProxyObject(
FILE: src/proxy/storage.ts
function clear (line 11) | function clear(storage: Record<string, any>) {
function getItem (line 17) | function getItem(storage: Record<string, any>) {
function removeItem (line 26) | function removeItem(storage: Record<string, any>) {
function setItem (line 32) | function setItem(
function createInstrumentations (line 54) | function createInstrumentations() {
function get (line 85) | function get(
function set (line 104) | function set(
function deleteProperty (line 112) | function deleteProperty(
function createProxyStorage (line 126) | function createProxyStorage(storage: StorageLike, name?: string) {
FILE: src/proxy/transform.ts
type Serializer (line 6) | interface Serializer<T> {
function decode (line 73) | function decode({
function encode (line 101) | function encode({
function simpleDecode (line 135) | function simpleDecode(data: string) {
function simpleEncode (line 142) | function simpleEncode(data: any) {
FILE: src/shared.ts
function setProxyStorage (line 9) | function setProxyStorage(storage: Record<string, any>, proxy: Record<str...
function clearProxyStorage (line 13) | function clearProxyStorage(storage: Record<string, any>): void {
function getProxyStorageProperty (line 18) | function getProxyStorageProperty(storage: Record<string, any>, property:...
function deleteProxyStorageProperty (line 28) | function deleteProxyStorageProperty(storage: Record<string, any>, proper...
function setProxyStorageProperty (line 34) | function setProxyStorageProperty(storage: Record<string, any>, property:...
function getRaw (line 40) | function getRaw(value: any) {
FILE: src/types.ts
type RawType (line 1) | type RawType = 'String' | 'Number' | 'BigInt' | 'Boolean' | 'Null' | 'Un...
type StorageLike (line 3) | interface StorageLike {
type StorageValue (line 12) | type StorageValue = string | number | bigint | boolean | null | undefine...
type StorageOptions (line 14) | interface StorageOptions {
type EffectMap (line 19) | type EffectMap = Map<string, Effect[]>
type EffectFn (line 20) | type EffectFn<V = any, OV = any> = (
type Effect (line 24) | interface Effect {
type StorageObject (line 29) | interface StorageObject {
type ExpiresType (line 35) | type ExpiresType = string | number | Date
FILE: src/utils.ts
function isSet (line 4) | function isSet(val: unknown): val is Set<any> {
function isMap (line 7) | function isMap(val: unknown): val is Map<any, any> {
function isDate (line 11) | function isDate(val: unknown): val is Date {
function isRegExp (line 14) | function isRegExp(val: unknown): val is RegExp {
function isURL (line 17) | function isURL(val: unknown): val is URL {
function isError (line 20) | function isError(val: unknown): val is Error {
function isFunction (line 23) | function isFunction(val: unknown): val is Function {
function isNumber (line 26) | function isNumber(val: unknown): val is number {
function isString (line 29) | function isString(val: unknown): val is string {
function isObject (line 32) | function isObject(val: unknown): val is Record<any, any> {
function isPromise (line 36) | function isPromise<T = any>(val: unknown): val is Promise<T> {
function isIntegerKey (line 44) | function isIntegerKey(key: unknown) {
function isStorage (line 51) | async function isStorage(storage: StorageLike) {
function isLocalStorage (line 55) | function isLocalStorage(storage: StorageLike) {
function isSessionStorage (line 59) | function isSessionStorage(storage: StorageLike) {
function getTypeString (line 63) | function getTypeString(value: unknown): string {
function getRawType (line 67) | function getRawType(value: unknown): RawType {
function hasChanged (line 71) | function hasChanged(value: any, oldValue: any): boolean {
function transformJSON (line 75) | function transformJSON(
function propertyIsInPrototype (line 87) | function propertyIsInPrototype(object: object, prototypeName: string) {
function hasOwn (line 92) | function hasOwn(val: object, key: string | symbol): key is keyof typeof ...
function transformEval (line 96) | function transformEval(code: string) {
function formatTime (line 105) | function formatTime(time: any) {
function pThen (line 117) | function pThen(getter: Function, callback: Function) {
FILE: tests/equal.spec.ts
function foo (line 55) | function foo() {
FILE: tests/expired.spec.ts
function delay (line 4) | async function delay(ms?: number) {
FILE: tests/global.d.ts
type Window (line 4) | interface Window {
FILE: tests/localforage.spec.ts
function delay (line 4) | async function delay(ms?: number) {
FILE: tests/serializer.spec.ts
function foo (line 253) | function foo() {
FILE: tests/storageOptions.spec.ts
function delay (line 4) | async function delay(ms?: number) {
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (104K chars).
[
{
"path": ".eslintignore",
"chars": 49,
"preview": ".DS_Store\nnode_modules\ndist\nplayground/stokado.js"
},
{
"path": ".gitignore",
"chars": 92,
"preview": ".DS_Store\nnode_modules\ndist\nplayground/index.html\nplayground/stokado.js\ntest-results\n.vscode"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2022 KID-joker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.ja.md",
"chars": 4048,
"preview": "```shell\n __ __ __ __\n ____ /\\ \\__ ___ /\\ \\/ \\ __ /\\ \\ __"
},
{
"path": "README.md",
"chars": 4714,
"preview": "```shell\n __ __ __ __\n ____ /\\ \\__ ___ /\\ \\/ \\ __ /\\ \\ __"
},
{
"path": "README.zh.md",
"chars": 3757,
"preview": "```shell\n __ __ __ __\n ____ /\\ \\__ ___ /\\ \\/ \\ __ /\\ \\ __"
},
{
"path": "eslint.config.js",
"chars": 329,
"preview": "import antfu from '@antfu/eslint-config'\n\nexport default antfu({\n ignores: ['.DS_Store', '**/.DS_Store/**', 'node_modul"
},
{
"path": "package.json",
"chars": 2175,
"preview": "{\n \"name\": \"stokado\",\n \"type\": \"module\",\n \"version\": \"3.0.1\",\n \"description\": \"stokado can proxy objects of any `sto"
},
{
"path": "playground/template.js",
"chars": 364,
"preview": "function template(options) {\n return `\n<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>"
},
{
"path": "playwright.config.ts",
"chars": 664,
"preview": "import type { PlaywrightTestConfig } from '@playwright/test'\nimport path from 'node:path'\nimport process from 'node:proc"
},
{
"path": "rollup.config.js",
"chars": 1977,
"preview": "import { readFileSync } from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { fileURLT"
},
{
"path": "src/extends/disposable.ts",
"chars": 989,
"preview": "import type { StorageObject } from '@/types'\nimport { encode } from '@/proxy/transform'\nimport { deleteProxyStoragePrope"
},
{
"path": "src/extends/expires.ts",
"chars": 1987,
"preview": "import type { ExpiresType, StorageObject, StorageOptions } from '@/types'\nimport { encode } from '@/proxy/transform'\nimp"
},
{
"path": "src/extends/options.ts",
"chars": 415,
"preview": "import type { StorageObject } from '@/types'\nimport { getProxyStorageProperty } from '@/shared'\nimport { isObject, pThen"
},
{
"path": "src/extends/watch.ts",
"chars": 2021,
"preview": "import type { Effect, EffectFn, EffectMap } from '@/types'\nimport { postMessage } from '@/proxy/broadcast'\nimport { hasC"
},
{
"path": "src/index.ts",
"chars": 77,
"preview": "export { createProxyStorage } from '@/proxy/storage'\nexport * from '@/types'\n"
},
{
"path": "src/proxy/broadcast.ts",
"chars": 1317,
"preview": "import type { StorageLike } from '@/types'\nimport { trigger } from '@/extends/watch'\nimport { decode, simpleDecode, simp"
},
{
"path": "src/proxy/object.ts",
"chars": 3849,
"preview": "import { getOptions } from '@/extends/options'\nimport { emit } from '@/extends/watch'\nimport { proxyObjectMap } from '@/"
},
{
"path": "src/proxy/storage.ts",
"chars": 4369,
"preview": "import type { StorageLike, StorageObject, StorageOptions } from '@/types'\nimport { checkDisposable, setDisposable } from"
},
{
"path": "src/proxy/transform.ts",
"chars": 3642,
"preview": "import type { RawType, StorageObject, StorageOptions } from '@/types'\nimport { createProxyObject } from '@/proxy/object'"
},
{
"path": "src/shared.ts",
"chars": 1643,
"preview": "import type { StorageObject } from '@/types'\nimport { decode } from '@/proxy/transform'\nimport { pThen } from '@/utils'\n"
},
{
"path": "src/types.ts",
"chars": 991,
"preview": "export type RawType = 'String' | 'Number' | 'BigInt' | 'Boolean' | 'Null' | 'Undefined' | 'Object' | 'Array' | 'Set' | '"
},
{
"path": "src/utils.ts",
"chars": 3504,
"preview": "import type { RawType, StorageLike } from '@/types'\n\nexport const isArray = Array.isArray\nexport function isSet(val: unk"
},
{
"path": "tests/base.spec.ts",
"chars": 2697,
"preview": "import { expect, test } from '@playwright/test'\nimport { decode, encode } from '@/proxy/transform'\nimport './global.d.ts"
},
{
"path": "tests/disposable.spec.ts",
"chars": 2809,
"preview": "import { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\ntest.describe('disposable', () => {\n test('setD"
},
{
"path": "tests/equal.spec.ts",
"chars": 2415,
"preview": "/* eslint-disable no-self-compare */\nimport { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\ntest.descri"
},
{
"path": "tests/expired.spec.ts",
"chars": 3239,
"preview": "import { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\nasync function delay(ms?: number) {\n return new"
},
{
"path": "tests/global.d.ts",
"chars": 284,
"preview": "import type { StorageLike } from '@/types'\n\ndeclare global {\n interface Window {\n stokado: {\n createProxyStorag"
},
{
"path": "tests/localforage.spec.ts",
"chars": 6148,
"preview": "import { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\nasync function delay(ms?: number) {\n return new"
},
{
"path": "tests/serializer.spec.ts",
"chars": 8711,
"preview": "import { expect, test } from '@playwright/test'\nimport { decode } from '@/proxy/transform'\nimport './global.d.ts'\n\ntest."
},
{
"path": "tests/storage.spec.ts",
"chars": 3504,
"preview": "import { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\ntest.describe('storage', () => {\n test('local f"
},
{
"path": "tests/storageOptions.spec.ts",
"chars": 5439,
"preview": "import { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\nasync function delay(ms?: number) {\n return new"
},
{
"path": "tests/subscribe.spec.ts",
"chars": 4909,
"preview": "import { expect, test } from '@playwright/test'\nimport './global.d.ts'\n\ntest.describe('subscribe', async () => {\n test("
},
{
"path": "tsconfig.json",
"chars": 506,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"ESNEXT\"],\n \"baseUrl\": \"./src\",\n \"rootDir\": \"."
},
{
"path": "v2.md",
"chars": 5105,
"preview": "```shell\n __ __ __ __\n ____ /\\ \\__ ___ /\\ \\/ \\ __ /\\ \\ __"
},
{
"path": "v2.zh.md",
"chars": 4025,
"preview": "```shell\n __ __ __ __\n ____ /\\ \\__ ___ /\\ \\/ \\ __ /\\ \\ __"
}
]
About this extraction
This page contains the full source code of the KID-joker/proxy-web-storage GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (91.6 KB), approximately 25.7k tokens, and a symbol index with 81 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.