@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.
Files changed (357) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +51 -0
  3. package/dist/adapter/accounts.d.ts +174 -0
  4. package/dist/adapter/accounts.d.ts.map +1 -0
  5. package/dist/adapter/accounts.js +653 -0
  6. package/dist/adapter/accounts.js.map +1 -0
  7. package/dist/adapter/bots.d.ts +142 -0
  8. package/dist/adapter/bots.d.ts.map +1 -0
  9. package/dist/adapter/bots.js +454 -0
  10. package/dist/adapter/bots.js.map +1 -0
  11. package/dist/adapter/host.d.ts +114 -0
  12. package/dist/adapter/host.d.ts.map +1 -0
  13. package/dist/adapter/host.js +90 -0
  14. package/dist/adapter/host.js.map +1 -0
  15. package/dist/adapter/login.d.ts +174 -0
  16. package/dist/adapter/login.d.ts.map +1 -0
  17. package/dist/adapter/login.js +345 -0
  18. package/dist/adapter/login.js.map +1 -0
  19. package/dist/adapter/registry.d.ts +102 -0
  20. package/dist/adapter/registry.d.ts.map +1 -0
  21. package/dist/adapter/registry.js +150 -0
  22. package/dist/adapter/registry.js.map +1 -0
  23. package/dist/config/core-config.d.ts +151 -0
  24. package/dist/config/core-config.d.ts.map +1 -0
  25. package/dist/config/core-config.js +338 -0
  26. package/dist/config/core-config.js.map +1 -0
  27. package/dist/config/schema.d.ts +449 -0
  28. package/dist/config/schema.d.ts.map +1 -0
  29. package/dist/config/schema.js +871 -0
  30. package/dist/config/schema.js.map +1 -0
  31. package/dist/config/store.d.ts +184 -0
  32. package/dist/config/store.d.ts.map +1 -0
  33. package/dist/config/store.js +428 -0
  34. package/dist/config/store.js.map +1 -0
  35. package/dist/config/yaml.d.ts +23 -0
  36. package/dist/config/yaml.d.ts.map +1 -0
  37. package/dist/config/yaml.js +108 -0
  38. package/dist/config/yaml.js.map +1 -0
  39. package/dist/http/client.d.ts +82 -0
  40. package/dist/http/client.d.ts.map +1 -0
  41. package/dist/http/client.js +658 -0
  42. package/dist/http/client.js.map +1 -0
  43. package/dist/index.d.ts +77 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +94 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/kernel/app.d.ts +244 -0
  48. package/dist/kernel/app.d.ts.map +1 -0
  49. package/dist/kernel/app.js +654 -0
  50. package/dist/kernel/app.js.map +1 -0
  51. package/dist/kernel/kv-sink.d.ts +34 -0
  52. package/dist/kernel/kv-sink.d.ts.map +1 -0
  53. package/dist/kernel/kv-sink.js +25 -0
  54. package/dist/kernel/kv-sink.js.map +1 -0
  55. package/dist/kernel/policy.d.ts +77 -0
  56. package/dist/kernel/policy.d.ts.map +1 -0
  57. package/dist/kernel/policy.js +99 -0
  58. package/dist/kernel/policy.js.map +1 -0
  59. package/dist/kernel/runtime.d.ts +117 -0
  60. package/dist/kernel/runtime.d.ts.map +1 -0
  61. package/dist/kernel/runtime.js +197 -0
  62. package/dist/kernel/runtime.js.map +1 -0
  63. package/dist/kernel/sql-sink.d.ts +36 -0
  64. package/dist/kernel/sql-sink.d.ts.map +1 -0
  65. package/dist/kernel/sql-sink.js +114 -0
  66. package/dist/kernel/sql-sink.js.map +1 -0
  67. package/dist/logger/format.d.ts +67 -0
  68. package/dist/logger/format.d.ts.map +1 -0
  69. package/dist/logger/format.js +218 -0
  70. package/dist/logger/format.js.map +1 -0
  71. package/dist/logger/index.d.ts +97 -0
  72. package/dist/logger/index.d.ts.map +1 -0
  73. package/dist/logger/index.js +363 -0
  74. package/dist/logger/index.js.map +1 -0
  75. package/dist/logger/rotate.d.ts +47 -0
  76. package/dist/logger/rotate.d.ts.map +1 -0
  77. package/dist/logger/rotate.js +242 -0
  78. package/dist/logger/rotate.js.map +1 -0
  79. package/dist/message/segment.d.ts +255 -0
  80. package/dist/message/segment.d.ts.map +1 -0
  81. package/dist/message/segment.js +510 -0
  82. package/dist/message/segment.js.map +1 -0
  83. package/dist/message/split.d.ts +34 -0
  84. package/dist/message/split.d.ts.map +1 -0
  85. package/dist/message/split.js +79 -0
  86. package/dist/message/split.js.map +1 -0
  87. package/dist/message/target.d.ts +29 -0
  88. package/dist/message/target.d.ts.map +1 -0
  89. package/dist/message/target.js +34 -0
  90. package/dist/message/target.js.map +1 -0
  91. package/dist/pipeline/cooldown.d.ts +84 -0
  92. package/dist/pipeline/cooldown.d.ts.map +1 -0
  93. package/dist/pipeline/cooldown.js +94 -0
  94. package/dist/pipeline/cooldown.js.map +1 -0
  95. package/dist/pipeline/dispatch.d.ts +98 -0
  96. package/dist/pipeline/dispatch.d.ts.map +1 -0
  97. package/dist/pipeline/dispatch.js +323 -0
  98. package/dist/pipeline/dispatch.js.map +1 -0
  99. package/dist/pipeline/event.d.ts +130 -0
  100. package/dist/pipeline/event.d.ts.map +1 -0
  101. package/dist/pipeline/event.js +538 -0
  102. package/dist/pipeline/event.js.map +1 -0
  103. package/dist/pipeline/middleware.d.ts +63 -0
  104. package/dist/pipeline/middleware.d.ts.map +1 -0
  105. package/dist/pipeline/middleware.js +174 -0
  106. package/dist/pipeline/middleware.js.map +1 -0
  107. package/dist/pipeline/prompt.d.ts +80 -0
  108. package/dist/pipeline/prompt.d.ts.map +1 -0
  109. package/dist/pipeline/prompt.js +155 -0
  110. package/dist/pipeline/prompt.js.map +1 -0
  111. package/dist/pipeline/router.d.ts +98 -0
  112. package/dist/pipeline/router.d.ts.map +1 -0
  113. package/dist/pipeline/router.js +489 -0
  114. package/dist/pipeline/router.js.map +1 -0
  115. package/dist/platform/detect.d.ts +24 -0
  116. package/dist/platform/detect.d.ts.map +1 -0
  117. package/dist/platform/detect.js +178 -0
  118. package/dist/platform/detect.js.map +1 -0
  119. package/dist/platform/paths.d.ts +38 -0
  120. package/dist/platform/paths.d.ts.map +1 -0
  121. package/dist/platform/paths.js +129 -0
  122. package/dist/platform/paths.js.map +1 -0
  123. package/dist/platform/system.d.ts +71 -0
  124. package/dist/platform/system.d.ts.map +1 -0
  125. package/dist/platform/system.js +242 -0
  126. package/dist/platform/system.js.map +1 -0
  127. package/dist/plugin/context.d.ts +92 -0
  128. package/dist/plugin/context.d.ts.map +1 -0
  129. package/dist/plugin/context.js +573 -0
  130. package/dist/plugin/context.js.map +1 -0
  131. package/dist/plugin/define.d.ts +92 -0
  132. package/dist/plugin/define.d.ts.map +1 -0
  133. package/dist/plugin/define.js +82 -0
  134. package/dist/plugin/define.js.map +1 -0
  135. package/dist/plugin/discover.d.ts +106 -0
  136. package/dist/plugin/discover.d.ts.map +1 -0
  137. package/dist/plugin/discover.js +254 -0
  138. package/dist/plugin/discover.js.map +1 -0
  139. package/dist/plugin/events.d.ts +109 -0
  140. package/dist/plugin/events.d.ts.map +1 -0
  141. package/dist/plugin/events.js +209 -0
  142. package/dist/plugin/events.js.map +1 -0
  143. package/dist/plugin/hooks.d.ts +266 -0
  144. package/dist/plugin/hooks.d.ts.map +1 -0
  145. package/dist/plugin/hooks.js +84 -0
  146. package/dist/plugin/hooks.js.map +1 -0
  147. package/dist/plugin/host.d.ts +160 -0
  148. package/dist/plugin/host.d.ts.map +1 -0
  149. package/dist/plugin/host.js +546 -0
  150. package/dist/plugin/host.js.map +1 -0
  151. package/dist/plugin/market.d.ts +264 -0
  152. package/dist/plugin/market.d.ts.map +1 -0
  153. package/dist/plugin/market.js +700 -0
  154. package/dist/plugin/market.js.map +1 -0
  155. package/dist/plugin/services.d.ts +116 -0
  156. package/dist/plugin/services.d.ts.map +1 -0
  157. package/dist/plugin/services.js +213 -0
  158. package/dist/plugin/services.js.map +1 -0
  159. package/dist/plugin/tar.d.ts +65 -0
  160. package/dist/plugin/tar.d.ts.map +1 -0
  161. package/dist/plugin/tar.js +277 -0
  162. package/dist/plugin/tar.js.map +1 -0
  163. package/dist/render/registry.d.ts +127 -0
  164. package/dist/render/registry.d.ts.map +1 -0
  165. package/dist/render/registry.js +306 -0
  166. package/dist/render/registry.js.map +1 -0
  167. package/dist/scheduler/index.d.ts +67 -0
  168. package/dist/scheduler/index.d.ts.map +1 -0
  169. package/dist/scheduler/index.js +279 -0
  170. package/dist/scheduler/index.js.map +1 -0
  171. package/dist/server/api.d.ts +179 -0
  172. package/dist/server/api.d.ts.map +1 -0
  173. package/dist/server/api.js +602 -0
  174. package/dist/server/api.js.map +1 -0
  175. package/dist/server/auth.d.ts +83 -0
  176. package/dist/server/auth.d.ts.map +1 -0
  177. package/dist/server/auth.js +178 -0
  178. package/dist/server/auth.js.map +1 -0
  179. package/dist/server/browse.d.ts +60 -0
  180. package/dist/server/browse.d.ts.map +1 -0
  181. package/dist/server/browse.js +187 -0
  182. package/dist/server/browse.js.map +1 -0
  183. package/dist/server/files.d.ts +27 -0
  184. package/dist/server/files.d.ts.map +1 -0
  185. package/dist/server/files.js +76 -0
  186. package/dist/server/files.js.map +1 -0
  187. package/dist/server/index.d.ts +189 -0
  188. package/dist/server/index.d.ts.map +1 -0
  189. package/dist/server/index.js +1123 -0
  190. package/dist/server/index.js.map +1 -0
  191. package/dist/server/table.d.ts +105 -0
  192. package/dist/server/table.d.ts.map +1 -0
  193. package/dist/server/table.js +289 -0
  194. package/dist/server/table.js.map +1 -0
  195. package/dist/store/index.d.ts +50 -0
  196. package/dist/store/index.d.ts.map +1 -0
  197. package/dist/store/index.js +120 -0
  198. package/dist/store/index.js.map +1 -0
  199. package/dist/store/json.d.ts +73 -0
  200. package/dist/store/json.d.ts.map +1 -0
  201. package/dist/store/json.js +174 -0
  202. package/dist/store/json.js.map +1 -0
  203. package/dist/store/kv.d.ts +126 -0
  204. package/dist/store/kv.d.ts.map +1 -0
  205. package/dist/store/kv.js +232 -0
  206. package/dist/store/kv.js.map +1 -0
  207. package/dist/store/level.d.ts +103 -0
  208. package/dist/store/level.d.ts.map +1 -0
  209. package/dist/store/level.js +119 -0
  210. package/dist/store/level.js.map +1 -0
  211. package/dist/store/memory.d.ts +61 -0
  212. package/dist/store/memory.d.ts.map +1 -0
  213. package/dist/store/memory.js +94 -0
  214. package/dist/store/memory.js.map +1 -0
  215. package/dist/store/sql.d.ts +142 -0
  216. package/dist/store/sql.d.ts.map +1 -0
  217. package/dist/store/sql.js +246 -0
  218. package/dist/store/sql.js.map +1 -0
  219. package/dist/testing/fake.d.ts +109 -0
  220. package/dist/testing/fake.d.ts.map +1 -0
  221. package/dist/testing/fake.js +196 -0
  222. package/dist/testing/fake.js.map +1 -0
  223. package/dist/testing/index.d.ts +16 -0
  224. package/dist/testing/index.d.ts.map +1 -0
  225. package/dist/testing/index.js +16 -0
  226. package/dist/testing/index.js.map +1 -0
  227. package/dist/testing/mock-adapter.d.ts +205 -0
  228. package/dist/testing/mock-adapter.d.ts.map +1 -0
  229. package/dist/testing/mock-adapter.js +309 -0
  230. package/dist/testing/mock-adapter.js.map +1 -0
  231. package/dist/util/deep.d.ts +108 -0
  232. package/dist/util/deep.d.ts.map +1 -0
  233. package/dist/util/deep.js +275 -0
  234. package/dist/util/deep.js.map +1 -0
  235. package/dist/util/defer.d.ts +107 -0
  236. package/dist/util/defer.d.ts.map +1 -0
  237. package/dist/util/defer.js +163 -0
  238. package/dist/util/defer.js.map +1 -0
  239. package/dist/util/dispose.d.ts +88 -0
  240. package/dist/util/dispose.d.ts.map +1 -0
  241. package/dist/util/dispose.js +143 -0
  242. package/dist/util/dispose.js.map +1 -0
  243. package/dist/util/duration.d.ts +28 -0
  244. package/dist/util/duration.d.ts.map +1 -0
  245. package/dist/util/duration.js +77 -0
  246. package/dist/util/duration.js.map +1 -0
  247. package/dist/util/fs.d.ts +110 -0
  248. package/dist/util/fs.d.ts.map +1 -0
  249. package/dist/util/fs.js +269 -0
  250. package/dist/util/fs.js.map +1 -0
  251. package/dist/util/id.d.ts +49 -0
  252. package/dist/util/id.d.ts.map +1 -0
  253. package/dist/util/id.js +87 -0
  254. package/dist/util/id.js.map +1 -0
  255. package/dist/util/lru.d.ts +74 -0
  256. package/dist/util/lru.d.ts.map +1 -0
  257. package/dist/util/lru.js +156 -0
  258. package/dist/util/lru.js.map +1 -0
  259. package/dist/util/queue.d.ts +64 -0
  260. package/dist/util/queue.d.ts.map +1 -0
  261. package/dist/util/queue.js +147 -0
  262. package/dist/util/queue.js.map +1 -0
  263. package/dist/util/text.d.ts +86 -0
  264. package/dist/util/text.d.ts.map +1 -0
  265. package/dist/util/text.js +182 -0
  266. package/dist/util/text.js.map +1 -0
  267. package/package.json +44 -0
  268. package/src/adapter/accounts.ts +758 -0
  269. package/src/adapter/bots.ts +582 -0
  270. package/src/adapter/host.ts +224 -0
  271. package/src/adapter/login.ts +481 -0
  272. package/src/adapter/registry.ts +202 -0
  273. package/src/config/core-config.test.ts +60 -0
  274. package/src/config/core-config.ts +401 -0
  275. package/src/config/schema.test.ts +179 -0
  276. package/src/config/schema.ts +1036 -0
  277. package/src/config/store.test.ts +245 -0
  278. package/src/config/store.ts +506 -0
  279. package/src/config/yaml.ts +119 -0
  280. package/src/http/client.test.ts +568 -0
  281. package/src/http/client.ts +777 -0
  282. package/src/index.ts +106 -0
  283. package/src/kernel/app.test.ts +267 -0
  284. package/src/kernel/app.ts +843 -0
  285. package/src/kernel/kv-sink.ts +58 -0
  286. package/src/kernel/policy.ts +120 -0
  287. package/src/kernel/runtime.test.ts +542 -0
  288. package/src/kernel/runtime.ts +307 -0
  289. package/src/kernel/sql-sink.ts +154 -0
  290. package/src/logger/format.ts +259 -0
  291. package/src/logger/index.ts +427 -0
  292. package/src/logger/rotate.ts +265 -0
  293. package/src/message/segment.test.ts +335 -0
  294. package/src/message/segment.ts +569 -0
  295. package/src/message/split.ts +97 -0
  296. package/src/message/target.ts +48 -0
  297. package/src/pipeline/cooldown.ts +136 -0
  298. package/src/pipeline/dispatch.ts +419 -0
  299. package/src/pipeline/event.ts +735 -0
  300. package/src/pipeline/middleware.test.ts +426 -0
  301. package/src/pipeline/middleware.ts +199 -0
  302. package/src/pipeline/prompt.ts +212 -0
  303. package/src/pipeline/router.test.ts +432 -0
  304. package/src/pipeline/router.ts +575 -0
  305. package/src/platform/detect.ts +178 -0
  306. package/src/platform/paths.ts +147 -0
  307. package/src/platform/system.test.ts +102 -0
  308. package/src/platform/system.ts +289 -0
  309. package/src/plugin/context.test.ts +459 -0
  310. package/src/plugin/context.ts +757 -0
  311. package/src/plugin/define.test.ts +120 -0
  312. package/src/plugin/define.ts +142 -0
  313. package/src/plugin/discover.test.ts +253 -0
  314. package/src/plugin/discover.ts +353 -0
  315. package/src/plugin/events.test.ts +162 -0
  316. package/src/plugin/events.ts +266 -0
  317. package/src/plugin/hooks.ts +373 -0
  318. package/src/plugin/host.test.ts +696 -0
  319. package/src/plugin/host.ts +738 -0
  320. package/src/plugin/market.test.ts +781 -0
  321. package/src/plugin/market.ts +870 -0
  322. package/src/plugin/services.test.ts +135 -0
  323. package/src/plugin/services.ts +261 -0
  324. package/src/plugin/tar.test.ts +158 -0
  325. package/src/plugin/tar.ts +309 -0
  326. package/src/render/registry.test.ts +127 -0
  327. package/src/render/registry.ts +439 -0
  328. package/src/scheduler/index.ts +357 -0
  329. package/src/server/api.test.ts +802 -0
  330. package/src/server/api.ts +869 -0
  331. package/src/server/auth.ts +201 -0
  332. package/src/server/browse.test.ts +193 -0
  333. package/src/server/browse.ts +240 -0
  334. package/src/server/files.ts +97 -0
  335. package/src/server/index.test.ts +566 -0
  336. package/src/server/index.ts +1386 -0
  337. package/src/server/table.ts +351 -0
  338. package/src/store/index.ts +156 -0
  339. package/src/store/json.ts +198 -0
  340. package/src/store/kv.test.ts +307 -0
  341. package/src/store/kv.ts +256 -0
  342. package/src/store/level.ts +167 -0
  343. package/src/store/memory.ts +109 -0
  344. package/src/store/sql.test.ts +204 -0
  345. package/src/store/sql.ts +324 -0
  346. package/src/testing/fake.ts +297 -0
  347. package/src/testing/index.ts +15 -0
  348. package/src/testing/mock-adapter.ts +588 -0
  349. package/src/util/deep.ts +272 -0
  350. package/src/util/defer.ts +211 -0
  351. package/src/util/dispose.ts +169 -0
  352. package/src/util/duration.ts +82 -0
  353. package/src/util/fs.ts +279 -0
  354. package/src/util/id.ts +94 -0
  355. package/src/util/lru.ts +185 -0
  356. package/src/util/queue.ts +167 -0
  357. package/src/util/text.ts +189 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * 模块职责:内嵌 LevelDB KV 驱动(默认驱动)
3
+ * 依赖方向:动态依赖可选原生模块 classic-level;依赖类型包
4
+ * 生命周期:`open()` 打开数据库目录,`close()` 关闭并 flush
5
+ * 注意事项:classic-level 是 **optionalDependency**(原生模块,Termux 上可能编不出来),
6
+ * 所以这里用动态 `import()` 且**不引用它的类型** —— 只声明自己真正用到的
7
+ * 那几个方法。这样在没装成功的机器上,整个内核依然能编译、能跑,
8
+ * 由 `openKv()` 自动降级到 json 驱动。
9
+ *
10
+ * 刻意不实现 `incr`:LevelDB 没有原子自增原语,硬写成 get+put 会在并发下
11
+ * 丢计数。留空让上层的按键串行队列接管,语义才是对的。
12
+ */
13
+ import type { KvBatchOp, KvDriver, StoredEnvelope } from "@yunzai-ng/types"
14
+ import { ensureDir } from "../util/fs.js"
15
+
16
+ /** classic-level 中本驱动实际用到的最小面 */
17
+ interface LevelDb {
18
+ /** 打开数据库 */
19
+ open(): Promise<void>
20
+ /** 关闭数据库 */
21
+ close(): Promise<void>
22
+ /** 读取,缺失返回 undefined(abstract-level 2.x 语义) */
23
+ get(key: string): Promise<StoredEnvelope | undefined>
24
+ /** 写入 */
25
+ put(key: string, value: StoredEnvelope): Promise<void>
26
+ /** 删除 */
27
+ del(key: string): Promise<void>
28
+ /** 批量写入/删除 */
29
+ batch(ops: { type: "put" | "del"; key: string; value?: StoredEnvelope }[]): Promise<void>
30
+ /** 范围迭代器 */
31
+ iterator(opts: { gte?: string }): LevelIterator
32
+ }
33
+
34
+ /** classic-level 的迭代器最小面 */
35
+ interface LevelIterator extends AsyncIterable<[string, StoredEnvelope]> {
36
+ /** 关闭迭代器,释放底层快照 */
37
+ close(): Promise<void>
38
+ }
39
+
40
+ /** classic-level 的构造签名 */
41
+ type LevelCtor = new (location: string, opts: { valueEncoding: "json" }) => LevelDb
42
+
43
+ /**
44
+ * 尝试加载 classic-level
45
+ * @returns 构造函数;模块不可用时 undefined
46
+ */
47
+ export async function loadLevel(): Promise<LevelCtor | undefined> {
48
+ try {
49
+ const mod = (await import("classic-level")) as unknown as { ClassicLevel?: LevelCtor }
50
+ return mod.ClassicLevel
51
+ } catch {
52
+ return undefined
53
+ }
54
+ }
55
+
56
+ /** 内嵌 LevelDB KV 驱动 */
57
+ export class LevelKvDriver implements KvDriver {
58
+ /** 驱动 id */
59
+ readonly id = "level"
60
+
61
+ /** 数据库目录 */
62
+ readonly #dir: string
63
+ /** 构造函数 */
64
+ readonly #ctor: LevelCtor
65
+ /** 数据库实例 */
66
+ #db: LevelDb | undefined
67
+
68
+ /**
69
+ * @param dir 数据库目录
70
+ * @param ctor classic-level 构造函数,由 `loadLevel()` 提供
71
+ */
72
+ constructor(dir: string, ctor: LevelCtor) {
73
+ this.#dir = dir
74
+ this.#ctor = ctor
75
+ }
76
+
77
+ /** 数据库目录 */
78
+ get dir(): string {
79
+ return this.#dir
80
+ }
81
+
82
+ /**
83
+ * 打开数据库
84
+ * @throws 目录被另一个进程锁住时(LevelDB 是单进程独占)
85
+ */
86
+ async open(): Promise<void> {
87
+ await ensureDir(this.#dir)
88
+ const db = new this.#ctor(this.#dir, { valueEncoding: "json" })
89
+ await db.open()
90
+ this.#db = db
91
+ }
92
+
93
+ /** 关闭数据库 */
94
+ async close(): Promise<void> {
95
+ const db = this.#db
96
+ this.#db = undefined
97
+ if (db) await db.close()
98
+ }
99
+
100
+ /**
101
+ * 读取
102
+ * @param key 完整键
103
+ * @returns 值信封;不存在时 undefined
104
+ */
105
+ async get(key: string): Promise<StoredEnvelope | undefined> {
106
+ return this.#require().get(key)
107
+ }
108
+
109
+ /**
110
+ * 写入
111
+ * @param key 完整键
112
+ * @param value 值信封
113
+ */
114
+ async set(key: string, value: StoredEnvelope): Promise<void> {
115
+ await this.#require().put(key, value)
116
+ }
117
+
118
+ /**
119
+ * 删除
120
+ * @param key 完整键
121
+ */
122
+ async del(key: string): Promise<void> {
123
+ await this.#require().del(key)
124
+ }
125
+
126
+ /**
127
+ * 批量操作(LevelDB 的原子批写)
128
+ * @param ops 操作列表
129
+ */
130
+ async batch(ops: KvBatchOp[]): Promise<void> {
131
+ if (ops.length === 0) return
132
+ await this.#require().batch(
133
+ ops.map(op => (op.op === "set" ? { type: "put" as const, key: op.key, value: op.value } : { type: "del" as const, key: op.key }))
134
+ )
135
+ }
136
+
137
+ /**
138
+ * 按前缀扫描
139
+ *
140
+ * 只给出 `gte` 而不给上界,依靠"键按字节序排列 ⇒ 同前缀的键连续"提前结束。
141
+ * 较自行计算 `lt` 上界更可靠:手工计算上界一旦遇到 emoji 一类四字节字符即会漏键。
142
+ * @param prefix 键前缀
143
+ * @returns 键值对迭代器
144
+ */
145
+ async *scan(prefix: string): AsyncIterableIterator<readonly [string, StoredEnvelope]> {
146
+ const it = this.#require().iterator({ gte: prefix })
147
+ try {
148
+ for await (const [key, value] of it) {
149
+ if (!key.startsWith(prefix)) break
150
+ yield [key, value]
151
+ }
152
+ } finally {
153
+ // 迭代器持有底层快照,不关会一直占着 sst 文件不让压缩
154
+ await it.close().catch(() => undefined)
155
+ }
156
+ }
157
+
158
+ /**
159
+ * 取已打开的数据库
160
+ * @returns 数据库实例
161
+ * @throws 未 open 时
162
+ */
163
+ #require(): LevelDb {
164
+ if (!this.#db) throw new Error("LevelDB 尚未打开,请先调用 open()")
165
+ return this.#db
166
+ }
167
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * 模块职责:内存 KV 驱动(测试与"仅内存"模式)
3
+ * 依赖方向:依赖类型包与 util/deep
4
+ * 生命周期:`close()` 后可再 `open()`
5
+ * 注意事项:**存取都做深拷贝**。若直接存引用,调用方在 set 之后修改对象会连带改掉
6
+ * "已落盘"的值;这样内存驱动下能跑通的代码换到 level/json 驱动就会出错,
7
+ * 属于最难查的一类问题。宁可多一次拷贝,也要让三种驱动语义完全一致。
8
+ */
9
+ import type { KvBatchOp, KvDriver, StoredEnvelope } from "@yunzai-ng/types"
10
+ import { deepClone } from "../util/deep.js"
11
+ import { isExpired } from "./kv.js"
12
+
13
+ /** 内存 KV 驱动 */
14
+ export class MemoryKvDriver implements KvDriver {
15
+ /** 驱动 id */
16
+ readonly id = "memory"
17
+
18
+ /** 数据表 */
19
+ readonly #map = new Map<string, StoredEnvelope>()
20
+
21
+ /** 键数量(含未清理的过期键),仅用于调试与面板展示 */
22
+ get size(): number {
23
+ return this.#map.size
24
+ }
25
+
26
+ /** 打开(内存驱动无需准备) */
27
+ async open(): Promise<void> {
28
+ // 无操作
29
+ }
30
+
31
+ /** 关闭并清空 */
32
+ async close(): Promise<void> {
33
+ this.#map.clear()
34
+ }
35
+
36
+ /**
37
+ * 读取
38
+ * @param key 完整键
39
+ * @returns 值信封的副本
40
+ */
41
+ async get(key: string): Promise<StoredEnvelope | undefined> {
42
+ const env = this.#map.get(key)
43
+ return env === undefined ? undefined : deepClone(env)
44
+ }
45
+
46
+ /**
47
+ * 写入
48
+ * @param key 完整键
49
+ * @param value 值信封
50
+ */
51
+ async set(key: string, value: StoredEnvelope): Promise<void> {
52
+ this.#map.set(key, deepClone(value))
53
+ }
54
+
55
+ /**
56
+ * 删除
57
+ * @param key 完整键
58
+ */
59
+ async del(key: string): Promise<void> {
60
+ this.#map.delete(key)
61
+ }
62
+
63
+ /**
64
+ * 批量操作
65
+ * @param ops 操作列表
66
+ */
67
+ async batch(ops: KvBatchOp[]): Promise<void> {
68
+ for (const op of ops) {
69
+ if (op.op === "set") this.#map.set(op.key, deepClone(op.value))
70
+ else this.#map.delete(op.key)
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 按前缀扫描
76
+ * @param prefix 键前缀
77
+ * @returns 键值对迭代器
78
+ */
79
+ async *scan(prefix: string): AsyncIterableIterator<readonly [string, StoredEnvelope]> {
80
+ // 先快照键集合:调用方在遍历中删除过期键是正常用法,
81
+ // 直接迭代 Map 会撞上"遍历中修改"的未定义顺序
82
+ for (const key of [...this.#map.keys()]) {
83
+ if (!key.startsWith(prefix)) continue
84
+ const env = this.#map.get(key)
85
+ if (env === undefined) continue
86
+ yield [key, deepClone(env)]
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 原生原子自增
92
+ *
93
+ * 单线程 JS 里同步的读改写天然原子,故内存驱动可以直接提供,
94
+ * 免掉上层的串行队列开销。
95
+ * @param key 完整键
96
+ * @param by 增量
97
+ * @param expireAt 首次创建时的过期时间点
98
+ * @returns 自增后的值
99
+ */
100
+ async incr(key: string, by: number, expireAt?: number): Promise<number> {
101
+ const env = this.#map.get(key)
102
+ const alive = env !== undefined && !isExpired(env)
103
+ const current = alive && typeof env.v === "number" ? env.v : 0
104
+ const next = current + by
105
+ const keep = alive ? env.e : expireAt
106
+ this.#map.set(key, keep === undefined ? { v: next } : { v: next, e: keep })
107
+ return next
108
+ }
109
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * 模块职责:SQLite 存储的行为测试
3
+ * 依赖方向:测试文件
4
+ * 生命周期:每个用例一个临时数据库文件
5
+ * 注意事项:better-sqlite3 是可选原生模块,装不上时整组跳过而不是报红。
6
+ * 重点覆盖异步事务的正确性 —— 这是本模块唯一有真实并发风险的地方。
7
+ */
8
+ import { mkdtemp, rm } from "node:fs/promises"
9
+ import { tmpdir } from "node:os"
10
+ import { join } from "node:path"
11
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
12
+ import type { SqlHandle, SqlMigration } from "@yunzai-ng/types"
13
+ import { loadSqlite, openSql, type SqlStore } from "./sql.js"
14
+
15
+ const ctor = await loadSqlite()
16
+
17
+ describe.skipIf(!ctor)("SQLite 存储", () => {
18
+ let dir: string
19
+ let store: SqlStore
20
+ let db: SqlHandle
21
+
22
+ beforeEach(async () => {
23
+ dir = await mkdtemp(join(tmpdir(), "yzng-sql-"))
24
+ const opened = await openSql({ file: join(dir, "data.db"), ctor })
25
+ store = opened!
26
+ db = store.handle
27
+ await db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, n INTEGER DEFAULT 0)")
28
+ })
29
+
30
+ afterEach(async () => {
31
+ store.close()
32
+ await rm(dir, { recursive: true, force: true })
33
+ })
34
+
35
+ it("run / all / get 的基本语义", async () => {
36
+ const inserted = await db.run("INSERT INTO t (name, n) VALUES (?, ?)", ["甲", 1])
37
+ expect(inserted.changes).toBe(1)
38
+ expect(Number(inserted.lastInsertRowid)).toBe(1)
39
+
40
+ await db.run("INSERT INTO t (name, n) VALUES (?, ?)", ["乙", 2])
41
+ expect(await db.all("SELECT name FROM t ORDER BY id")).toEqual([{ name: "甲" }, { name: "乙" }])
42
+ expect(await db.get("SELECT n FROM t WHERE name = ?", ["乙"])).toEqual({ n: 2 })
43
+ // 无结果统一返回 undefined,而不是 null
44
+ expect(await db.get("SELECT n FROM t WHERE name = ?", ["丙"])).toBeUndefined()
45
+ })
46
+
47
+ it("不带参数的多语句脚本可以一次执行(迁移常用)", async () => {
48
+ await db.run(`
49
+ CREATE TABLE a (id INTEGER PRIMARY KEY);
50
+ CREATE INDEX idx_a ON a (id);
51
+ `)
52
+ const tables = await db.all<{ name: string }>("SELECT name FROM sqlite_master WHERE type = 'table'")
53
+ expect(tables.map(r => r.name)).toContain("a")
54
+ })
55
+
56
+ it("语句里的分号字面量不会被误判成多语句", async () => {
57
+ await db.run("INSERT INTO t (name) VALUES (?)", ["带;分号"])
58
+ expect(await db.get("SELECT name FROM t WHERE name = ?", ["带;分号"])).toEqual({ name: "带;分号" })
59
+ })
60
+
61
+ it("事务提交后数据可见", async () => {
62
+ await db.transaction(async tx => {
63
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["甲"])
64
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["乙"])
65
+ })
66
+ expect(await db.get("SELECT COUNT(*) AS c FROM t")).toEqual({ c: 2 })
67
+ })
68
+
69
+ it("事务体抛错时整体回滚,错误照原样冒出", async () => {
70
+ await expect(
71
+ db.transaction(async tx => {
72
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["甲"])
73
+ throw new Error("业务失败")
74
+ })
75
+ ).rejects.toThrow("业务失败")
76
+
77
+ expect(await db.get("SELECT COUNT(*) AS c FROM t")).toEqual({ c: 0 })
78
+ })
79
+
80
+ it("事务里 await 别的异步操作也不会串台(互斥锁生效)", async () => {
81
+ // 两个事务同时开:若没有互斥锁,第二个 BEGIN 会撞上
82
+ // "cannot start a transaction within a transaction"
83
+ const one = db.transaction(async tx => {
84
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["甲"])
85
+ await new Promise(r => setTimeout(r, 30))
86
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["乙"])
87
+ })
88
+ const two = db.transaction(async tx => {
89
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["丙"])
90
+ })
91
+
92
+ await Promise.all([one, two])
93
+ expect(await db.get("SELECT COUNT(*) AS c FROM t")).toEqual({ c: 3 })
94
+ })
95
+
96
+ it("并发事务不会互相回滚对方的写入", async () => {
97
+ await db.run("INSERT INTO t (id, name, n) VALUES (1, '计数', 0)")
98
+
99
+ const bump = () =>
100
+ db.transaction(async tx => {
101
+ const row = await tx.get<{ n: number }>("SELECT n FROM t WHERE id = 1")
102
+ await tx.run("UPDATE t SET n = ? WHERE id = 1", [(row?.n ?? 0) + 1])
103
+ })
104
+
105
+ await Promise.all(Array.from({ length: 20 }, bump))
106
+ expect(await db.get("SELECT n FROM t WHERE id = 1")).toEqual({ n: 20 })
107
+ })
108
+
109
+ it("嵌套事务用 SAVEPOINT:内层回滚不牵连外层", async () => {
110
+ await db.transaction(async tx => {
111
+ await tx.run("INSERT INTO t (name) VALUES (?)", ["外层"])
112
+ await expect(
113
+ tx.transaction(async inner => {
114
+ await inner.run("INSERT INTO t (name) VALUES (?)", ["内层"])
115
+ throw new Error("内层失败")
116
+ })
117
+ ).rejects.toThrow("内层失败")
118
+ })
119
+
120
+ const names = await db.all<{ name: string }>("SELECT name FROM t")
121
+ expect(names.map(r => r.name)).toEqual(["外层"])
122
+ })
123
+
124
+ it("迁移按版本号依次执行且幂等", async () => {
125
+ const log: string[] = []
126
+ const migrations: SqlMigration[] = [
127
+ {
128
+ version: 2,
129
+ name: "加索引",
130
+ up: async tx => {
131
+ log.push("v2")
132
+ await tx.run("CREATE INDEX idx_name ON t (name)")
133
+ }
134
+ },
135
+ {
136
+ version: 1,
137
+ name: "建表",
138
+ up: async tx => {
139
+ log.push("v1")
140
+ await tx.run("CREATE TABLE m (id INTEGER PRIMARY KEY)")
141
+ }
142
+ }
143
+ ]
144
+
145
+ await db.migrate(migrations)
146
+ expect(log).toEqual(["v1", "v2"])
147
+
148
+ // 再跑一次什么都不该发生
149
+ await db.migrate(migrations)
150
+ expect(log).toEqual(["v1", "v2"])
151
+
152
+ // 新增一条只跑新的
153
+ migrations.push({
154
+ version: 3,
155
+ name: "再加一张表",
156
+ up: async tx => {
157
+ log.push("v3")
158
+ await tx.run("CREATE TABLE m2 (id INTEGER PRIMARY KEY)")
159
+ }
160
+ })
161
+ await db.migrate(migrations)
162
+ expect(log).toEqual(["v1", "v2", "v3"])
163
+ })
164
+
165
+ it("迁移失败时该条回滚,且不会被记为已应用", async () => {
166
+ const migrations: SqlMigration[] = [
167
+ {
168
+ version: 1,
169
+ name: "会失败的迁移",
170
+ up: async tx => {
171
+ await tx.run("CREATE TABLE ok (id INTEGER PRIMARY KEY)")
172
+ await tx.run("这不是 SQL")
173
+ }
174
+ }
175
+ ]
176
+
177
+ await expect(db.migrate(migrations)).rejects.toThrow()
178
+
179
+ const tables = await db.all<{ name: string }>("SELECT name FROM sqlite_master WHERE type = 'table'")
180
+ expect(tables.map(r => r.name)).not.toContain("ok")
181
+ expect(await db.get("SELECT COUNT(*) AS c FROM _yzng_migrations")).toEqual({ c: 0 })
182
+ })
183
+
184
+ it("版本号重复直接报错,而不是随机执行一条", async () => {
185
+ await expect(
186
+ db.migrate([
187
+ { version: 1, name: "甲", up: async () => undefined },
188
+ { version: 1, name: "乙", up: async () => undefined }
189
+ ])
190
+ ).rejects.toThrow(/重复/)
191
+ })
192
+
193
+ it("重开文件后数据仍在", async () => {
194
+ await db.run("INSERT INTO t (name) VALUES (?)", ["持久"])
195
+ const file = store.file
196
+ store.close()
197
+
198
+ const reopened = (await openSql({ file, ctor }))!
199
+ expect(await reopened.handle.get("SELECT name FROM t")).toEqual({ name: "持久" })
200
+ reopened.close()
201
+ // afterEach 会再 close 一次,close 必须可重复调用
202
+ store = reopened
203
+ })
204
+ })