@yunzai-ng/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:KV 层的契约测试(同一套用例覆盖 memory / json / level 三种驱动)
|
|
3
|
+
* 依赖方向:测试文件
|
|
4
|
+
* 生命周期:每个驱动一个临时目录
|
|
5
|
+
* 注意事项:**同一套用例覆盖全部驱动**是此处的关键设计 —— 任何驱动只要通过这份契约
|
|
6
|
+
* 即可互换,第三方存储插件将来同样沿用这份用例。
|
|
7
|
+
* level 驱动依赖可选原生模块,无法安装时该组用例整体跳过而非报告失败。
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
10
|
+
import { tmpdir } from "node:os"
|
|
11
|
+
import { join } from "node:path"
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
13
|
+
import type { KvDriver, KvNamespace } from "@yunzai-ng/types"
|
|
14
|
+
import { Kv } from "./kv.js"
|
|
15
|
+
import { MemoryKvDriver } from "./memory.js"
|
|
16
|
+
import { JsonKvDriver } from "./json.js"
|
|
17
|
+
import { LevelKvDriver, loadLevel } from "./level.js"
|
|
18
|
+
|
|
19
|
+
/** 待测驱动的构造方式 */
|
|
20
|
+
interface DriverCase {
|
|
21
|
+
/** 名称 */
|
|
22
|
+
name: string
|
|
23
|
+
/** 造一个未 open 的驱动;返回 undefined 表示该驱动不可用 */
|
|
24
|
+
make(dir: string): Promise<KvDriver | undefined>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const levelCtor = await loadLevel()
|
|
28
|
+
|
|
29
|
+
const cases: DriverCase[] = [
|
|
30
|
+
{ name: "memory", make: async () => new MemoryKvDriver() },
|
|
31
|
+
{ name: "json", make: async dir => new JsonKvDriver(join(dir, "kv.json")) },
|
|
32
|
+
{
|
|
33
|
+
name: "level",
|
|
34
|
+
make: async dir => (levelCtor ? new LevelKvDriver(join(dir, "kv"), levelCtor) : undefined)
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
for (const driverCase of cases) {
|
|
39
|
+
describe(`KV 契约 · ${driverCase.name}`, () => {
|
|
40
|
+
let dir: string
|
|
41
|
+
let driver: KvDriver
|
|
42
|
+
let kv: KvNamespace
|
|
43
|
+
let available = true
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
dir = await mkdtemp(join(tmpdir(), `yzng-kv-${driverCase.name}-`))
|
|
47
|
+
const made = await driverCase.make(dir)
|
|
48
|
+
if (!made) {
|
|
49
|
+
available = false
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
driver = made
|
|
53
|
+
await driver.open()
|
|
54
|
+
kv = new Kv(driver)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
if (available) await driver.close()
|
|
59
|
+
await rm(dir, { recursive: true, force: true })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/** 该驱动不可用时跳过单个用例 */
|
|
63
|
+
const runIf = (fn: () => Promise<void>) => async () => {
|
|
64
|
+
if (!available) return
|
|
65
|
+
await fn()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
it(
|
|
69
|
+
"读写删的基本语义",
|
|
70
|
+
runIf(async () => {
|
|
71
|
+
expect(await kv.get("a")).toBeUndefined()
|
|
72
|
+
await kv.set("a", { n: 1, list: ["x"] })
|
|
73
|
+
expect(await kv.get("a")).toEqual({ n: 1, list: ["x"] })
|
|
74
|
+
expect(await kv.has("a")).toBe(true)
|
|
75
|
+
|
|
76
|
+
await kv.del("a")
|
|
77
|
+
expect(await kv.has("a")).toBe(false)
|
|
78
|
+
// 删不存在的键不应抛错
|
|
79
|
+
await kv.del("a")
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
it(
|
|
84
|
+
"getOr 只在缺失时给默认值,false / 0 / null 都要原样返回",
|
|
85
|
+
runIf(async () => {
|
|
86
|
+
expect(await kv.getOr("missing", "兜底")).toBe("兜底")
|
|
87
|
+
await kv.set("flag", false)
|
|
88
|
+
await kv.set("zero", 0)
|
|
89
|
+
await kv.set("nil", null)
|
|
90
|
+
expect(await kv.getOr("flag", true)).toBe(false)
|
|
91
|
+
expect(await kv.getOr("zero", 99)).toBe(0)
|
|
92
|
+
expect(await kv.getOr("nil", "兜底")).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
it(
|
|
97
|
+
"存进去的对象与外部引用解耦(改外部变量不影响已存的值)",
|
|
98
|
+
runIf(async () => {
|
|
99
|
+
const obj = { list: [1, 2] }
|
|
100
|
+
await kv.set("k", obj)
|
|
101
|
+
obj.list.push(3)
|
|
102
|
+
expect(await kv.get("k")).toEqual({ list: [1, 2] })
|
|
103
|
+
|
|
104
|
+
// 读出来的对象也不能是内部引用
|
|
105
|
+
const read = (await kv.get<{ list: number[] }>("k"))!
|
|
106
|
+
read.list.push(99)
|
|
107
|
+
expect(await kv.get("k")).toEqual({ list: [1, 2] })
|
|
108
|
+
})
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
it(
|
|
112
|
+
"TTL 到期后视为不存在,并且不再出现在遍历里",
|
|
113
|
+
runIf(async () => {
|
|
114
|
+
// TTL 取 150ms 而非 20ms:20ms 短于 LevelDB 在负载中的一次写读往返,
|
|
115
|
+
// 于是 `set` 之后紧接的 `get` 会**合法地**读到已过期 —— 断言因此偶发失败,
|
|
116
|
+
// 而失败信息(expected undefined to be 1)看起来像 TTL 实现坏了
|
|
117
|
+
await kv.set("short", 1, { ttl: 150 })
|
|
118
|
+
await kv.set("long", 2, { ttl: "1h" })
|
|
119
|
+
expect(await kv.get("short")).toBe(1)
|
|
120
|
+
|
|
121
|
+
await new Promise(r => setTimeout(r, 300))
|
|
122
|
+
expect(await kv.get("short")).toBeUndefined()
|
|
123
|
+
expect(await kv.has("short")).toBe(false)
|
|
124
|
+
|
|
125
|
+
const keys: string[] = []
|
|
126
|
+
for await (const key of kv.keys()) keys.push(key)
|
|
127
|
+
expect(keys).toEqual(["long"])
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
it(
|
|
132
|
+
"ttl() 报告剩余时间:永久为 Infinity,缺失为 -1",
|
|
133
|
+
runIf(async () => {
|
|
134
|
+
await kv.set("forever", 1)
|
|
135
|
+
await kv.set("temp", 1, { ttl: "10s" })
|
|
136
|
+
expect(await kv.ttl("forever")).toBe(Number.POSITIVE_INFINITY)
|
|
137
|
+
expect(await kv.ttl("nope")).toBe(-1)
|
|
138
|
+
|
|
139
|
+
const left = await kv.ttl("temp")
|
|
140
|
+
expect(left).toBeGreaterThan(8000)
|
|
141
|
+
expect(left).toBeLessThanOrEqual(10_000)
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
it(
|
|
146
|
+
"incr 在并发下不丢计数(内嵌驱动无 Redis INCR 可依赖,须自行保证)",
|
|
147
|
+
runIf(async () => {
|
|
148
|
+
const results = await Promise.all(Array.from({ length: 50 }, () => kv.incr("count")))
|
|
149
|
+
expect(await kv.get("count")).toBe(50)
|
|
150
|
+
// 每次调用都应拿到唯一的序号,说明确实串行
|
|
151
|
+
expect(new Set(results).size).toBe(50)
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
it(
|
|
156
|
+
"incr 的 ttl 只在首次创建时生效,自增不会续命",
|
|
157
|
+
runIf(async () => {
|
|
158
|
+
await kv.incr("c", 1, { ttl: "10s" })
|
|
159
|
+
const first = await kv.ttl("c")
|
|
160
|
+
await new Promise(r => setTimeout(r, 30))
|
|
161
|
+
await kv.incr("c", 1)
|
|
162
|
+
const second = await kv.ttl("c")
|
|
163
|
+
expect(await kv.get("c")).toBe(2)
|
|
164
|
+
expect(second).toBeLessThan(first)
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
it(
|
|
169
|
+
"incr 遇到过期键从 0 重新开始",
|
|
170
|
+
runIf(async () => {
|
|
171
|
+
// 同上:20ms 会与驱动的写读往返赛跑
|
|
172
|
+
await kv.incr("c", 5, { ttl: 150 })
|
|
173
|
+
await new Promise(r => setTimeout(r, 300))
|
|
174
|
+
expect(await kv.incr("c")).toBe(1)
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
it(
|
|
179
|
+
"遍历只看得到本命名空间的键,且键名已剥掉前缀",
|
|
180
|
+
runIf(async () => {
|
|
181
|
+
const a = kv.sub("plugin-a")
|
|
182
|
+
const b = kv.sub("plugin-b")
|
|
183
|
+
await a.set("k1", 1)
|
|
184
|
+
await a.set("k2", 2)
|
|
185
|
+
await b.set("k1", 3)
|
|
186
|
+
|
|
187
|
+
const entries: [string, unknown][] = []
|
|
188
|
+
for await (const entry of a.entries()) entries.push([entry[0], entry[1]])
|
|
189
|
+
expect(entries.sort()).toEqual([
|
|
190
|
+
["k1", 1],
|
|
191
|
+
["k2", 2]
|
|
192
|
+
])
|
|
193
|
+
expect(await b.get("k1")).toBe(3)
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
it(
|
|
198
|
+
"同名子命名空间不会相互覆盖键",
|
|
199
|
+
runIf(async () => {
|
|
200
|
+
const a = kv.sub("x").sub("y")
|
|
201
|
+
const b = kv.sub("xy")
|
|
202
|
+
await a.set("k", "深层")
|
|
203
|
+
await b.set("k", "同名")
|
|
204
|
+
expect(await a.get("k")).toBe("深层")
|
|
205
|
+
expect(await b.get("k")).toBe("同名")
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
it(
|
|
210
|
+
"按前缀遍历与按前缀清空",
|
|
211
|
+
runIf(async () => {
|
|
212
|
+
await kv.set("user:1", "a")
|
|
213
|
+
await kv.set("user:2", "b")
|
|
214
|
+
await kv.set("group:1", "c")
|
|
215
|
+
|
|
216
|
+
const userKeys: string[] = []
|
|
217
|
+
for await (const key of kv.keys("user:")) userKeys.push(key)
|
|
218
|
+
expect(userKeys.sort()).toEqual(["user:1", "user:2"])
|
|
219
|
+
|
|
220
|
+
expect(await kv.clear("user:")).toBe(2)
|
|
221
|
+
expect(await kv.get("user:1")).toBeUndefined()
|
|
222
|
+
expect(await kv.get("group:1")).toBe("c")
|
|
223
|
+
|
|
224
|
+
expect(await kv.clear()).toBe(1)
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
it(
|
|
229
|
+
"清空子命名空间不影响父命名空间的其他键",
|
|
230
|
+
runIf(async () => {
|
|
231
|
+
await kv.set("root-key", 1)
|
|
232
|
+
const sub = kv.sub("child")
|
|
233
|
+
await sub.set("k", 2)
|
|
234
|
+
|
|
235
|
+
expect(await sub.clear()).toBe(1)
|
|
236
|
+
expect(await kv.get("root-key")).toBe(1)
|
|
237
|
+
})
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
it(
|
|
241
|
+
"拒绝含分隔符的子命名空间名(否则两个不同子空间会拼出同一前缀)",
|
|
242
|
+
runIf(async () => {
|
|
243
|
+
expect(() => kv.sub("a:b")).toThrow(/不能含/)
|
|
244
|
+
expect(() => kv.sub("")).toThrow(/不能为空/)
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
it(
|
|
249
|
+
"键里含中文与 emoji 也能正确前缀扫描",
|
|
250
|
+
runIf(async () => {
|
|
251
|
+
// level 按字节序排列,四字节字符曾是"自行计算上界"写法的典型漏键场景
|
|
252
|
+
await kv.set("用户:甲", 1)
|
|
253
|
+
await kv.set("用户:🙂", 2)
|
|
254
|
+
await kv.set("别的:乙", 3)
|
|
255
|
+
|
|
256
|
+
const keys: string[] = []
|
|
257
|
+
for await (const key of kv.keys("用户:")) keys.push(key)
|
|
258
|
+
expect(keys.sort()).toEqual(["用户:甲", "用户:🙂"].sort())
|
|
259
|
+
})
|
|
260
|
+
)
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
describe("JSON 驱动的落盘", () => {
|
|
265
|
+
let dir: string
|
|
266
|
+
|
|
267
|
+
beforeEach(async () => {
|
|
268
|
+
dir = await mkdtemp(join(tmpdir(), "yzng-kv-json-persist-"))
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
afterEach(async () => {
|
|
272
|
+
await rm(dir, { recursive: true, force: true })
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it("close() 后重新打开,数据仍在", async () => {
|
|
276
|
+
const file = join(dir, "kv.json")
|
|
277
|
+
const first = new JsonKvDriver(file)
|
|
278
|
+
await first.open()
|
|
279
|
+
const kv1 = new Kv(first)
|
|
280
|
+
await kv1.set("a", { hello: "世界" })
|
|
281
|
+
await kv1.set("gone", 1, { ttl: 10 })
|
|
282
|
+
await first.close()
|
|
283
|
+
|
|
284
|
+
await new Promise(r => setTimeout(r, 30))
|
|
285
|
+
|
|
286
|
+
const second = new JsonKvDriver(file)
|
|
287
|
+
await second.open()
|
|
288
|
+
const kv2 = new Kv(second)
|
|
289
|
+
expect(await kv2.get("a")).toEqual({ hello: "世界" })
|
|
290
|
+
// 载入时就清掉过期键,不等到被访问
|
|
291
|
+
expect(await kv2.get("gone")).toBeUndefined()
|
|
292
|
+
expect(second.size).toBe(1)
|
|
293
|
+
await second.close()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it("文件损坏时抛出可诊断的错误并留下备份", async () => {
|
|
297
|
+
const file = join(dir, "kv.json")
|
|
298
|
+
const { writeFile, readdir } = await import("node:fs/promises")
|
|
299
|
+
await writeFile(file, "{ 这不是 json", "utf8")
|
|
300
|
+
|
|
301
|
+
const driver = new JsonKvDriver(file)
|
|
302
|
+
await expect(driver.open()).rejects.toThrow(/解析失败/)
|
|
303
|
+
|
|
304
|
+
const files = await readdir(dir)
|
|
305
|
+
expect(files.some(f => f.includes(".corrupt-"))).toBe(true)
|
|
306
|
+
})
|
|
307
|
+
})
|
package/src/store/kv.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:驱动之上的命名空间化 KV 视图(前缀隔离、TTL、原子自增、前缀遍历)
|
|
3
|
+
* 依赖方向:依赖 util/{duration,queue}、类型包;不认识任何具体驱动
|
|
4
|
+
* 生命周期:与驱动同寿;`Kv` 自身无需回收
|
|
5
|
+
* 注意事项:三条规则 ——
|
|
6
|
+
*
|
|
7
|
+
* 1) **过期为读时判定**,不用定时器扫描 —— 那会在低内存设备上白耗 CPU。读到过期键
|
|
8
|
+
* 即视为不存在并一并删除,遍历时同样机会式清理。
|
|
9
|
+
* 2) **`incr` 在驱动没有原生原子操作时退化为"按键串行的读改写"。** LevelDB 没有 INCR,
|
|
10
|
+
* 直接 `get` + `set` 会让同一秒内的并发消息相互覆盖、计数偏小。`KeyedQueue` 把同一个键
|
|
11
|
+
* 的操作排队,进程内语义正确;跨进程原子性不予承诺(单进程架构无此需要)。
|
|
12
|
+
* 3) **每个插件的键自带 `plugin:<name>:` 前缀**,插件之间不可能访问到对方的键 ——
|
|
13
|
+
* 隔离由前缀保证,不靠命名约定。
|
|
14
|
+
*/
|
|
15
|
+
import type { DurationLike, KvDriver, KvNamespace, KvSetOptions, StoredEnvelope } from "@yunzai-ng/types"
|
|
16
|
+
import { parseDuration } from "../util/duration.js"
|
|
17
|
+
import { KeyedQueue } from "../util/queue.js"
|
|
18
|
+
|
|
19
|
+
/** 命名空间分隔符 */
|
|
20
|
+
const SEP = ":"
|
|
21
|
+
|
|
22
|
+
/** 单次遍历中最多清理多少个过期键(有界,避免遍历大库时删除列表耗尽内存) */
|
|
23
|
+
const SWEEP_LIMIT = 256
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 命名空间化的 KV 视图
|
|
27
|
+
*
|
|
28
|
+
* 由 `openKv()` 创建根视图,插件通过 `ctx.kv` 拿到 `plugin:<name>:` 子视图。
|
|
29
|
+
*/
|
|
30
|
+
export class Kv implements KvNamespace {
|
|
31
|
+
/** 本命名空间的键前缀 */
|
|
32
|
+
readonly prefix: string
|
|
33
|
+
|
|
34
|
+
/** 底层驱动 */
|
|
35
|
+
readonly #driver: KvDriver
|
|
36
|
+
/** 自增串行队列,与同一驱动的所有视图共享 */
|
|
37
|
+
readonly #queue: KeyedQueue
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param driver 底层驱动
|
|
41
|
+
* @param prefix 键前缀(含结尾分隔符,根视图为空串)
|
|
42
|
+
* @param queue 自增串行队列;派生子视图时必须沿用父级的队列,
|
|
43
|
+
* 否则同一个键在父子视图里会落到两条队列上,串行保证失效
|
|
44
|
+
*/
|
|
45
|
+
constructor(driver: KvDriver, prefix = "", queue = new KeyedQueue()) {
|
|
46
|
+
this.#driver = driver
|
|
47
|
+
this.prefix = prefix
|
|
48
|
+
this.#queue = queue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 读取
|
|
53
|
+
* @param key 命名空间内的键
|
|
54
|
+
* @returns 值;不存在或已过期时 undefined
|
|
55
|
+
*/
|
|
56
|
+
async get<T = unknown>(key: string): Promise<T | undefined> {
|
|
57
|
+
const full = this.#full(key)
|
|
58
|
+
const env = await this.#driver.get(full)
|
|
59
|
+
if (env === undefined) return undefined
|
|
60
|
+
if (isExpired(env)) {
|
|
61
|
+
// 读到过期键即予删除:不删除则遍历与容量统计均会被无效数据污染
|
|
62
|
+
await this.#driver.del(full).catch(() => undefined)
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
return env.v as T
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 读取,不存在时返回默认值
|
|
70
|
+
* @param key 命名空间内的键
|
|
71
|
+
* @param fallback 默认值
|
|
72
|
+
* @returns 值或默认值
|
|
73
|
+
*/
|
|
74
|
+
async getOr<T>(key: string, fallback: T): Promise<T> {
|
|
75
|
+
const value = await this.get<T>(key)
|
|
76
|
+
return value === undefined ? fallback : value
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 写入
|
|
81
|
+
* @param key 命名空间内的键
|
|
82
|
+
* @param value 值,必须可 JSON 序列化
|
|
83
|
+
* @param opts 写入选项
|
|
84
|
+
*/
|
|
85
|
+
async set<T = unknown>(key: string, value: T, opts?: KvSetOptions): Promise<void> {
|
|
86
|
+
await this.#driver.set(this.#full(key), envelope(value, opts?.ttl))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 删除
|
|
91
|
+
* @param key 命名空间内的键
|
|
92
|
+
*/
|
|
93
|
+
async del(key: string): Promise<void> {
|
|
94
|
+
await this.#driver.del(this.#full(key))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 判断是否存在且未过期
|
|
99
|
+
* @param key 命名空间内的键
|
|
100
|
+
* @returns 是否存在
|
|
101
|
+
*/
|
|
102
|
+
async has(key: string): Promise<boolean> {
|
|
103
|
+
return (await this.get(key)) !== undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 原子自增
|
|
108
|
+
*
|
|
109
|
+
* 驱动提供原生 `incr` 就直接用;否则按键串行地读改写。
|
|
110
|
+
* @param key 命名空间内的键
|
|
111
|
+
* @param by 增量,默认 1
|
|
112
|
+
* @param opts 写入选项;`ttl` 只在键首次创建时生效(与 Redis 的 INCR 语义一致:
|
|
113
|
+
* 自增不会刷新过期时间,否则热点计数器永不过期)
|
|
114
|
+
* @returns 自增后的值
|
|
115
|
+
*/
|
|
116
|
+
async incr(key: string, by = 1, opts?: KvSetOptions): Promise<number> {
|
|
117
|
+
const full = this.#full(key)
|
|
118
|
+
const expireAt = expiryOf(opts?.ttl)
|
|
119
|
+
|
|
120
|
+
if (this.#driver.incr) return this.#driver.incr(full, by, expireAt)
|
|
121
|
+
|
|
122
|
+
return this.#queue.run(full, async () => {
|
|
123
|
+
const env = await this.#driver.get(full)
|
|
124
|
+
const alive = env !== undefined && !isExpired(env)
|
|
125
|
+
const current = alive && typeof env.v === "number" ? env.v : 0
|
|
126
|
+
const next = current + by
|
|
127
|
+
|
|
128
|
+
// 已存在的键保留原过期时间点,新建的键才套用传入的 ttl
|
|
129
|
+
const keep = alive ? env.e : expireAt
|
|
130
|
+
await this.#driver.set(full, keep === undefined ? { v: next } : { v: next, e: keep })
|
|
131
|
+
return next
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 读取剩余存活毫秒数
|
|
137
|
+
* @param key 命名空间内的键
|
|
138
|
+
* @returns 剩余毫秒;永不过期返回 Infinity;不存在或已过期返回 -1
|
|
139
|
+
*/
|
|
140
|
+
async ttl(key: string): Promise<number> {
|
|
141
|
+
const env = await this.#driver.get(this.#full(key))
|
|
142
|
+
if (env === undefined || isExpired(env)) return -1
|
|
143
|
+
if (env.e === undefined) return Number.POSITIVE_INFINITY
|
|
144
|
+
return env.e - Date.now()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 遍历键(不含命名空间前缀)
|
|
149
|
+
* @param prefix 命名空间内的额外前缀
|
|
150
|
+
* @returns 键的异步迭代器
|
|
151
|
+
*/
|
|
152
|
+
async *keys(prefix = ""): AsyncIterableIterator<string> {
|
|
153
|
+
for await (const [key] of this.entries(prefix)) yield key
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 遍历键值
|
|
158
|
+
* @param prefix 命名空间内的额外前缀
|
|
159
|
+
* @returns 键值对的异步迭代器
|
|
160
|
+
*/
|
|
161
|
+
async *entries<T = unknown>(prefix = ""): AsyncIterableIterator<readonly [string, T]> {
|
|
162
|
+
const base = this.#full(prefix)
|
|
163
|
+
const expired: string[] = []
|
|
164
|
+
try {
|
|
165
|
+
for await (const [full, env] of this.#driver.scan(base)) {
|
|
166
|
+
if (isExpired(env)) {
|
|
167
|
+
if (expired.length < SWEEP_LIMIT) expired.push(full)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
yield [full.slice(this.prefix.length), env.v as T]
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
// 在迭代结束后再删:遍历过程中改动底层游标在部分驱动上是未定义行为。
|
|
174
|
+
// 提前 break 也会走到这里,已收集的过期键同样被清理。
|
|
175
|
+
if (expired.length > 0) await this.#delMany(expired)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 清空(可按前缀)
|
|
181
|
+
* @param prefix 命名空间内的额外前缀,省略则清空整个命名空间
|
|
182
|
+
* @returns 删除的键数量
|
|
183
|
+
*/
|
|
184
|
+
async clear(prefix = ""): Promise<number> {
|
|
185
|
+
const base = this.#full(prefix)
|
|
186
|
+
const keys: string[] = []
|
|
187
|
+
for await (const [full] of this.#driver.scan(base)) keys.push(full)
|
|
188
|
+
await this.#delMany(keys)
|
|
189
|
+
return keys.length
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 派生子命名空间
|
|
194
|
+
* @param name 子命名空间名
|
|
195
|
+
* @returns 新的 KV 视图,与父级共享驱动与自增队列
|
|
196
|
+
* @throws 当名字为空或含分隔符时(否则两个不同的子空间可能拼出同一个前缀)
|
|
197
|
+
*/
|
|
198
|
+
sub(name: string): KvNamespace {
|
|
199
|
+
if (name === "") throw new Error("子命名空间名不能为空")
|
|
200
|
+
if (name.includes(SEP)) throw new Error(`子命名空间名不能含 "${SEP}":${name}`)
|
|
201
|
+
return new Kv(this.#driver, `${this.prefix}${name}${SEP}`, this.#queue)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 拼出完整键
|
|
206
|
+
* @param key 命名空间内的键
|
|
207
|
+
* @returns 完整键
|
|
208
|
+
*/
|
|
209
|
+
#full(key: string): string {
|
|
210
|
+
return `${this.prefix}${key}`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 批量删除,能用 batch 就用 batch
|
|
215
|
+
* @param keys 完整键列表
|
|
216
|
+
*/
|
|
217
|
+
async #delMany(keys: readonly string[]): Promise<void> {
|
|
218
|
+
if (keys.length === 0) return
|
|
219
|
+
if (this.#driver.batch) {
|
|
220
|
+
await this.#driver.batch(keys.map(key => ({ op: "del" as const, key })))
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
for (const key of keys) await this.#driver.del(key)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 判断信封是否已过期
|
|
229
|
+
* @param env 值信封
|
|
230
|
+
* @returns 是否过期
|
|
231
|
+
*/
|
|
232
|
+
export function isExpired(env: StoredEnvelope): boolean {
|
|
233
|
+
return env.e !== undefined && env.e <= Date.now()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 计算过期时间点
|
|
238
|
+
* @param ttl 存活时长
|
|
239
|
+
* @returns 毫秒时间戳;不过期时 undefined
|
|
240
|
+
*/
|
|
241
|
+
export function expiryOf(ttl: DurationLike | undefined): number | undefined {
|
|
242
|
+
if (ttl === undefined) return undefined
|
|
243
|
+
const ms = parseDuration(ttl, 0)
|
|
244
|
+
return ms > 0 ? Date.now() + ms : undefined
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 组装值信封
|
|
249
|
+
* @param value 值
|
|
250
|
+
* @param ttl 存活时长
|
|
251
|
+
* @returns 值信封
|
|
252
|
+
*/
|
|
253
|
+
export function envelope(value: unknown, ttl?: DurationLike): StoredEnvelope {
|
|
254
|
+
const e = expiryOf(ttl)
|
|
255
|
+
return e === undefined ? { v: value } : { v: value, e }
|
|
256
|
+
}
|