@yunzai-ng/core 0.1.0 → 0.2.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/dist/config/core-config.d.ts +1 -1
- package/dist/platform/paths.d.ts +10 -0
- package/dist/platform/paths.d.ts.map +1 -1
- package/dist/platform/paths.js +50 -7
- package/dist/platform/paths.js.map +1 -1
- package/dist/store/sql.js +4 -4
- package/package.json +12 -2
- package/src/adapter/accounts.ts +1 -1
- package/src/adapter/bots.ts +1 -1
- package/src/adapter/host.ts +1 -1
- package/src/adapter/login.ts +1 -1
- package/src/kernel/app.ts +843 -843
- package/src/kernel/policy.ts +120 -120
- package/src/kernel/runtime.ts +1 -1
- package/src/pipeline/event.ts +1 -1
- package/src/platform/paths.test.ts +166 -0
- package/src/platform/paths.ts +52 -7
- package/src/platform/system.ts +1 -1
- package/src/plugin/market.ts +1 -1
- package/src/plugin/services.ts +1 -1
- package/src/render/registry.ts +1 -1
- package/src/scheduler/index.ts +1 -1
- package/src/server/auth.ts +1 -1
- package/src/server/files.ts +1 -1
- package/src/server/index.ts +1 -1
- package/src/store/index.ts +156 -156
- package/src/store/kv.ts +256 -256
- package/src/store/sql.ts +324 -324
package/src/store/kv.ts
CHANGED
|
@@ -1,256 +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
|
-
}
|
|
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
|
+
}
|