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