Full Code of KID-joker/proxy-web-storage for AI

main 62f6ee4db5f3 cached
36 files
91.6 KB
25.7k tokens
81 symbols
1 requests
Download .txt
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`:设置一次性的指定项名字。
Download .txt
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
Download .txt
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.

Copied to clipboard!