chanjs 2.5.9 → 2.6.1

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/base/AOP.MD ADDED
@@ -0,0 +1,318 @@
1
+ # AOP 切面编程使用文档
2
+
3
+ ## 一、概念说明
4
+
5
+ AOP(面向切面编程)就是在函数执行前后插入额外逻辑,不修改原函数代码。
6
+
7
+ **本质**:给函数加一层"皮",在函数执行前、后、出错时插入自定义逻辑。
8
+
9
+ **优势**:
10
+ - 不改原函数代码
11
+ - 复用逻辑(日志、缓存、权限检查等可以统一处理)
12
+ - 按需开启(想用就用 wrap,不想用就不 wrap)
13
+ - 适用场景 :日志、缓存、权限检查、性能监控、事务管理等
14
+
15
+
16
+ ## 二、定义切面函数
17
+
18
+ ### 1. 日志切面
19
+ ```javascript
20
+ const logAspect = async ({ ctx, methodName, args, result, error }) => {
21
+ if (error) {
22
+ console.error(`[ERROR] ${methodName} 出错:`, error.message);
23
+ return;
24
+ }
25
+
26
+ if (result) {
27
+ console.log(`[SUCCESS] ${methodName} 执行完成,结果:`, result);
28
+ } else {
29
+ console.log(`[START] ${methodName} 开始执行,参数:`, args);
30
+ }
31
+ };
32
+ ```
33
+
34
+ ### 2. 缓存切面
35
+ ```javascript
36
+ const cacheAspect = async ({ ctx, args, methodName, originalMethod, params }) => {
37
+ const key = `${methodName}:${JSON.stringify(args)}`;
38
+
39
+ // 尝试从缓存获取
40
+ const cached = await ctx.cache?.get(key);
41
+ if (cached) {
42
+ console.log(`[CACHE HIT] ${key}`);
43
+ return cached;
44
+ }
45
+
46
+ // 执行原方法
47
+ const result = await originalMethod.apply(ctx, args);
48
+
49
+ // 写入缓存
50
+ const ttl = params?.ttl || 3600;
51
+ await ctx.cache?.set(key, result, ttl);
52
+ console.log(`[CACHE SET] ${key}, TTL: ${ttl}s`);
53
+
54
+ return result;
55
+ };
56
+ ```
57
+
58
+ ### 3. 权限切面
59
+ ```javascript
60
+ const authAspect = async ({ ctx, args, params }) => {
61
+ const requiredRole = params?.role || 'user';
62
+ const userRole = ctx.user?.role || 'guest';
63
+
64
+ if (userRole !== requiredRole) {
65
+ throw new Error(`权限不足,需要: ${requiredRole},当前: ${userRole}`);
66
+ }
67
+
68
+ console.log(`[AUTH] ${userRole} 通过权限检查`);
69
+ };
70
+ ```
71
+
72
+ ### 4. 性能监控切面
73
+ ```javascript
74
+ const perfAspect = async ({ ctx, methodName, args, originalMethod }) => {
75
+ const startTime = Date.now();
76
+
77
+ try {
78
+ const result = await originalMethod.apply(ctx, args);
79
+ const cost = Date.now() - startTime;
80
+ console.log(`[PERF] ${methodName} 耗时: ${cost}ms`);
81
+ return result;
82
+ } catch (error) {
83
+ const cost = Date.now() - startTime;
84
+ console.log(`[PERF] ${methodName} 耗时: ${cost}ms (出错)`);
85
+ throw error;
86
+ }
87
+ };
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 三、注册切面
93
+
94
+ ```javascript
95
+ import { aop } from 'Chan';
96
+
97
+ // 注册切面
98
+ aop.set('log', logAspect);
99
+ aop.set('cache', cacheAspect);
100
+ aop.set('auth', authAspect);
101
+ aop.set('perf', perfAspect);
102
+ ```
103
+
104
+ ---
105
+
106
+ ## 四、绑定切面到实例
107
+
108
+ ### 1. 基本绑定
109
+ ```javascript
110
+ import UserService from './service/UserService.js';
111
+ import { aop } from 'Chan';
112
+
113
+ const userService = new UserService();
114
+
115
+ // 绑定切面
116
+ aop.wrap(userService, {
117
+ getUser: [
118
+ { type: 'before', log: true },
119
+ { type: 'after', log: true }
120
+ ]
121
+ });
122
+
123
+ // 正常调用(切面自动生效)
124
+ const user = await userService.getUser(1);
125
+ ```
126
+
127
+ ### 2. 多个切面组合
128
+ ```javascript
129
+ aop.wrap(userService, {
130
+ getUser: [
131
+ { type: 'before', log: true },
132
+ { type: 'before', auth: { role: 'admin' }},
133
+ { type: 'after', log: true },
134
+ { type: 'after', cache: { ttl: 1800 }}
135
+ ],
136
+ createUser: [
137
+ { type: 'before', log: true },
138
+ { type: 'after', log: true }
139
+ ]
140
+ });
141
+ ```
142
+
143
+ ### 3. 禁用切面
144
+ ```javascript
145
+ aop.wrap(userService, {
146
+ getUser: [
147
+ { type: 'before', log: true },
148
+ { type: 'after', log: false } // 禁用 after 日志
149
+ ]
150
+ });
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 五、完整使用示例
156
+
157
+ ### 场景:用户服务带日志、缓存、权限控制
158
+
159
+ ```javascript
160
+ // 1. 定义切面
161
+ const logAspect = async ({ ctx, methodName, args, result, error }) => {
162
+ if (error) {
163
+ console.error(`[ERROR] ${methodName}:`, error.message);
164
+ return;
165
+ }
166
+
167
+ console.log(`[${methodName}] 参数:`, args, '结果:', result);
168
+ };
169
+
170
+ const cacheAspect = async ({ ctx, args, methodName, originalMethod, params }) => {
171
+ const key = `${methodName}:${JSON.stringify(args)}`;
172
+ const cached = await ctx.cache?.get(key);
173
+ if (cached) return cached;
174
+
175
+ const result = await originalMethod.apply(ctx, args);
176
+ await ctx.cache?.set(key, result, params?.ttl || 3600);
177
+ return result;
178
+ };
179
+
180
+ const authAspect = async ({ ctx, params }) => {
181
+ if (params?.role && ctx.user?.role !== params.role) {
182
+ throw new Error(`权限不足`);
183
+ }
184
+ };
185
+
186
+ // 2. 注册切面
187
+ import { aop } from 'Chan';
188
+ aop.set('log', logAspect);
189
+ aop.set('cache', cacheAspect);
190
+ aop.set('auth', authAspect);
191
+
192
+ // 3. 绑定切面
193
+ class UserService {
194
+ constructor(cache, user) {
195
+ this.cache = cache;
196
+ this.user = user;
197
+ }
198
+
199
+ async getUser(id) {
200
+ return this.db('users').where({ id }).first();
201
+ }
202
+
203
+ async deleteUser(id) {
204
+ return this.db('users').where({ id }).delete();
205
+ }
206
+ }
207
+
208
+ const userService = new UserService(cache, currentUser);
209
+ aop.wrap(userService, {
210
+ getUser: [
211
+ { type: 'before', log: true },
212
+ { type: 'before', auth: { role: 'admin' }},
213
+ { type: 'after', cache: { ttl: 1800 }}
214
+ ],
215
+ deleteUser: [
216
+ { type: 'before', log: true },
217
+ { type: 'before', auth: { role: 'admin' }}
218
+ ]
219
+ });
220
+
221
+ // 4. 正常调用
222
+ const user = await userService.getUser(1); // 自动执行:before → 权限 → 原方法 → 缓存 → after → 日志
223
+ await userService.deleteUser(1); // 自动执行:before → 权限 → 原方法 → after → 日志
224
+ ```
225
+
226
+ ---
227
+
228
+ ## 六、切面类型说明
229
+
230
+ | 类型 | 时机 | 用途 | 参数 |
231
+ |------|------|------|------|
232
+ | `before` | 函数执行前 | 日志、权限检查、参数验证 | `ctx`, `args`, `methodName`, `originalMethod` |
233
+ | `after` | 函数执行后 | 缓存、日志、数据清理 | `ctx`, `args`, `methodName`, `originalMethod`, `result` |
234
+ | `error` | 函数出错时 | 错误日志、错误处理、降级 | `ctx`, `args`, `methodName`, `originalMethod`, `error` |
235
+
236
+ ### 切面参数说明
237
+
238
+ ```javascript
239
+ {
240
+ ctx, // 上下文对象(实例本身),可访问实例属性和方法
241
+ methodName, // 方法名称
242
+ args, // 方法参数数组
243
+ originalMethod, // 原始方法函数
244
+ result, // 方法执行结果(after/error 类型才有)
245
+ error, // 错误对象(error 类型才有)
246
+ params // 自定义参数(通过配置传递)
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 七、配置格式说明
253
+
254
+ ### 基本格式
255
+ ```javascript
256
+ {
257
+ methodName: {
258
+ type: 'before', // 切面类型:before/after/error
259
+ enabled: true, // 是否启用(默认true,设为false禁用)
260
+ 切面名: true, // 启用切面(无参数)
261
+ 切面名: { 参数 } // 启用切面(带参数)
262
+ }
263
+ }
264
+ ```
265
+
266
+ ### 数组格式(多个切面)
267
+ ```javascript
268
+ {
269
+ methodName: [
270
+ { type: 'before', log: true },
271
+ { type: 'before', auth: { role: 'admin' }},
272
+ { type: 'after', cache: { ttl: 3600 }}
273
+ ]
274
+ }
275
+ ```
276
+
277
+ ---
278
+
279
+ ## 八、工具方法
280
+
281
+ ```javascript
282
+ // 注册切面
283
+ aop.set('log', logAspect);
284
+
285
+ // 获取切面
286
+ const logFn = aop.get('log');
287
+
288
+ // 移除切面
289
+ aop.remove('log');
290
+
291
+ // 清空所有切面
292
+ aop.clear();
293
+ ```
294
+
295
+ ---
296
+
297
+ ## 九、注意事项
298
+
299
+ 1. **切面函数必须是异步函数**
300
+ - 因为原方法可能是异步的
301
+ - 切面函数需要 `await` 原方法
302
+
303
+ 2. **参数浅拷贝**
304
+ - `args: [...args]` 避免修改原参数
305
+ - `params: { ...value }` 避免修改配置对象
306
+
307
+ 3. **错误处理**
308
+ - error 切面执行后会重新抛出异常
309
+ - 不会阻断原方法的错误流程
310
+
311
+ 4. **切面执行顺序**
312
+ - before 切面按配置顺序执行
313
+ - after 切面按配置顺序执行
314
+ - error 切面按配置顺序执行
315
+
316
+ 5. **around 切面已移除**
317
+ - 当前版本仅支持 before/after/error 三种类型
318
+ - around 类型已简化为 before + after 组合
package/base/Aop.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @description: 轻量级切面类 用于注册和执行切面函数
3
+ * @class Aop
4
+ * @property {Map} aspects - 存储注册的切面函数
5
+ * @property {Set} types - 切面类型集合(内置关键字)
6
+ */
7
+ export class Aop {
8
+ // 定义内置的切面类型关键字(避免和切面名称冲突)
9
+ static TYPES = new Set(['type', 'enabled']);
10
+ // 支持的切面类型(仅保留常用的3种)
11
+ static SUPPORTED_TYPES = new Set(['before', 'after', 'error']);
12
+
13
+ constructor() {
14
+ this.aspects = new Map();
15
+ }
16
+
17
+ /**
18
+ * 注册切面
19
+ * @param {string} name - 切面名称
20
+ * @param {Function} fn - 切面函数
21
+ * @returns {Aop} - 当前实例(链式调用)
22
+ * @throws {TypeError} - 入参类型错误时抛出
23
+ */
24
+ set(name, fn) {
25
+ if (typeof name !== 'string' || !name.trim()) {
26
+ throw new TypeError(`切面名称 ${name} 必须是非空字符串`);
27
+ }
28
+ if (typeof fn !== 'function') {
29
+ throw new TypeError(`切面 ${name} 必须是函数,当前类型:${typeof fn}`);
30
+ }
31
+ this.aspects.set(name.trim(), fn);
32
+ return this;
33
+ }
34
+
35
+ /**
36
+ * 获取已注册的切面函数
37
+ * @param {string} name - 切面名称
38
+ * @returns {Function|null} - 切面函数(不存在则返回null)
39
+ */
40
+ get(name) {
41
+ return this.aspects.get(name?.trim()) || null;
42
+ }
43
+
44
+ /**
45
+ * 移除指定切面
46
+ * @param {string} name - 切面名称
47
+ * @returns {Aop} - 当前实例
48
+ */
49
+ remove(name) {
50
+ this.aspects.delete(name?.trim());
51
+ return this;
52
+ }
53
+
54
+ /**
55
+ * 清空所有切面
56
+ * @returns {Aop} - 当前实例
57
+ */
58
+ clear() {
59
+ this.aspects.clear();
60
+ return this;
61
+ }
62
+
63
+ /**
64
+ * 绑定切面到实例方法
65
+ * @param {object} instance - 目标实例(如 new Controller())
66
+ * @param {object} config - 切面配置 { methodName: [{ type: 'before', enabled: true, log: true }] }
67
+ * @returns {object} - 绑定后的实例对象
68
+ * @throws {TypeError} - 入参类型错误时抛出
69
+ */
70
+ wrap(instance, config) {
71
+ if (typeof instance !== 'object' || instance === null) {
72
+ throw new TypeError('instance 必须是非空对象');
73
+ }
74
+ if (typeof config !== 'object' || config === null) {
75
+ throw new TypeError('config 必须是非空对象');
76
+ }
77
+
78
+ Object.entries(config).forEach(([methodName, rules]) => {
79
+ const originalMethod = instance[methodName];
80
+ if (typeof originalMethod !== 'function') {
81
+ console.warn(`[AOP警告] 实例不存在方法 ${methodName},跳过切面绑定`);
82
+ return;
83
+ }
84
+
85
+ // 统一规则为数组 + 过滤禁用规则 + 过滤不支持的类型
86
+ const ruleList = Array.isArray(rules) ? rules : [rules];
87
+ const validRules = ruleList.filter(rule => {
88
+ return rule?.enabled !== false && Aop.SUPPORTED_TYPES.has(rule?.type);
89
+ });
90
+
91
+ // 无有效规则时直接返回原方法
92
+ if (validRules.length === 0) return;
93
+
94
+ // 包装原方法(仅保留 before/after/error 逻辑)
95
+ instance[methodName] = async (...args) => {
96
+ const ctx = instance;
97
+ let result;
98
+
99
+ try {
100
+ // 执行前置切面
101
+ await this._executeAspectByType(ctx, validRules, 'before', args, originalMethod);
102
+
103
+ // 执行原方法
104
+ result = await Reflect.apply(originalMethod, ctx, args);
105
+
106
+ // 执行后置切面(仅无异常时执行)
107
+ await this._executeAspectByType(ctx, validRules, 'after', args, originalMethod, result);
108
+ } catch (error) {
109
+ // 执行异常切面
110
+ await this._executeAspectByType(ctx, validRules, 'error', args, originalMethod, null, error);
111
+ throw error; // 重新抛出异常,不阻断流程
112
+ }
113
+
114
+ return result;
115
+ };
116
+ });
117
+
118
+ return instance;
119
+ }
120
+
121
+ /**
122
+ * 按类型批量执行切面(私有方法)
123
+ * @private
124
+ * @param {object} ctx - 上下文
125
+ * @param {Array} validRules - 有效规则列表
126
+ * @param {string} type - 切面类型(before/after/error)
127
+ * @param {Array} args - 方法参数
128
+ * @param {Function} originalMethod - 原方法
129
+ * @param {*} result - 方法执行结果
130
+ * @param {Error} error - 方法执行错误
131
+ */
132
+ async _executeAspectByType(ctx, validRules, type, args, originalMethod, result = null, error = null) {
133
+ const rulesOfType = validRules.filter(rule => rule.type === type);
134
+ for (const rule of rulesOfType) {
135
+ await this._executeSingleRule(ctx, rule, args, originalMethod, result, error);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 执行单个切面规则(私有方法)
141
+ * @private
142
+ * @param {object} ctx - 上下文
143
+ * @param {object} rule - 单个切面规则
144
+ * @param {Array} args - 方法参数
145
+ * @param {Function} originalMethod - 原方法
146
+ * @param {*} result - 方法执行结果
147
+ * @param {Error} error - 方法执行错误
148
+ */
149
+ async _executeSingleRule(ctx, rule, args, originalMethod, result = null, error = null) {
150
+ for (const [key, value] of Object.entries(rule)) {
151
+ // 跳过内置关键字 + 空值
152
+ if (Aop.TYPES.has(key) || !value) continue;
153
+
154
+ const aspectFn = this.get(key);
155
+ if (!aspectFn) {
156
+ console.warn(`[AOP警告] 未注册切面 ${key},跳过执行`);
157
+ continue;
158
+ }
159
+
160
+ // 构造切面入参(浅拷贝避免原数据被修改)
161
+ const aspectParams = {
162
+ ctx,
163
+ methodName: originalMethod.name || 'anonymous',
164
+ args: [...args],
165
+ originalMethod,
166
+ result,
167
+ error,
168
+ params: typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}
169
+ };
170
+
171
+ await aspectFn(aspectParams);
172
+ }
173
+ }
174
+ }
175
+
176
+ // 全局单例
177
+ export const aop = new Aop();
178
+ export default Aop;
@@ -0,0 +1,81 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Paths } from '../config/paths.js';
4
+
5
+ export class Container {
6
+ constructor(type = 'service') {
7
+ this.map = new Map();
8
+ this.type = type;
9
+ this.baseDir = Paths.modulesPath;
10
+
11
+ // 只添加 config 属性,db 由 Service 自己管理
12
+ this.config = Chan.config;
13
+ }
14
+
15
+ /**
16
+ * 获取模块下的组件实例
17
+ * @param {string} moduleName - 模块名 ,例如:'web','api'
18
+ * @param {string} fileName - 文件名
19
+ * @returns {Promise<any>} - 组件实例
20
+ */
21
+ async get(moduleName, fileName) {
22
+ const key = `${moduleName}.${fileName}`;
23
+
24
+ if (this.map.has(key)) {
25
+ return this.map.get(key);
26
+ }
27
+
28
+ try {
29
+ await this.loadFile(moduleName, fileName);
30
+ const obj = this.map.get(key);
31
+ if (!obj) {
32
+ console.log(`文件 ${fileName}.js 加载后组件实例为空`);
33
+ }
34
+ return obj;
35
+ } catch (err) {
36
+ console.error(`获取模块 ${moduleName} 下文件 ${fileName}.js 对应的组件失败:${err.message}`);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * 设置模块下的组件实例
42
+ * @param {string} moduleName - 模块名 ,例如:'web','api'
43
+ * @param {string} fileName - 文件名
44
+ * @param {any} obj - 组件实例
45
+ * @returns {Container} - 当前实例
46
+ */
47
+ set(moduleName, fileName, obj) {
48
+ const key = `${moduleName}.${fileName}`;
49
+ this.map.set(key, obj);
50
+ return this;
51
+ }
52
+
53
+ /**
54
+ * 加载模块下的组件实例
55
+ * @param {string} moduleName - 模块名 ,例如:'web','api'
56
+ * @param {string} fileName - 文件名
57
+ * @returns {Promise<any>} - 组件实例
58
+ */
59
+ async loadFile(moduleName, fileName) {
60
+ const filePath = path.resolve(
61
+ this.baseDir,
62
+ moduleName,
63
+ this.type,
64
+ `${fileName}.js`
65
+ );
66
+
67
+ if (!fs.existsSync(filePath)) {
68
+ console.log(`模块 ${moduleName} 下 ${this.type} 目录中未找到文件:${fileName}.js`);
69
+ return null;
70
+ }
71
+
72
+ const module = await import(`file://${filePath}`);
73
+ const obj = module.default;
74
+
75
+ const key = `${moduleName}.${fileName}`;
76
+ this.map.set(key, obj);
77
+ return obj;
78
+ }
79
+ }
80
+
81
+ export default Container;
@@ -1,11 +1,14 @@
1
1
  import { success, fail } from "../utils/response.js";
2
+ import Container from "./Container.js";
2
3
 
3
4
  /**
4
5
  * 控制器基类
5
6
  * 提供统一的响应格式和常用方法
6
7
  */
7
- export default class Controller {
8
- constructor() {}
8
+ export default class Controller extends Container {
9
+ constructor() {
10
+ super('controller');
11
+ }
9
12
 
10
13
  /**
11
14
  * 返回成功响应
package/base/Event.js ADDED
@@ -0,0 +1,49 @@
1
+ import { EventEmitter } from 'events';
2
+
3
+ export class Event extends EventEmitter {
4
+ constructor() {
5
+ super();
6
+ }
7
+
8
+ /**
9
+ * 注册事件监听器
10
+ * @param {string} eventName - 事件名
11
+ * @param {*} listener - 监听器函数
12
+ * @returns {Event} - 当前实例
13
+ */
14
+ on(eventName, listener) {
15
+ return super.on(eventName, listener);
16
+ }
17
+
18
+ /**
19
+ * 触发事件
20
+ * @param {string} eventName - 事件名
21
+ * @param {*} data - 事件数据
22
+ * @returns {Event} - 当前实例
23
+ */
24
+ emit(eventName, data) {
25
+ return super.emit(eventName, data);
26
+ }
27
+
28
+ /**
29
+ * 注销事件监听器
30
+ * @param {string} eventName - 事件名
31
+ * @param {*} listener - 监听器函数
32
+ * @returns {Event} - 当前实例
33
+ */
34
+ off(eventName, listener) {
35
+ return super.off(eventName, listener);
36
+ }
37
+
38
+ /**
39
+ * 注销所有事件监听器
40
+ * @param {string} eventName - 事件名
41
+ * @returns {Event} - 当前实例
42
+ */
43
+ removeAllListeners(eventName) {
44
+ return super.removeAllListeners(eventName);
45
+ }
46
+ }
47
+
48
+ export const event = new Event();
49
+ export default Event;
package/base/Service.js CHANGED
@@ -2,21 +2,35 @@
2
2
  * 数据库服务基类
3
3
  * 提供常用的数据库操作方法,包括增删改查、分页查询、事务等
4
4
  */
5
- import { formatTime, formatDateFields } from "../helper/time.js";
5
+ import { formatDateFields } from "../helper/time.js";
6
+ import Container from "./Container.js";
6
7
 
7
- class Service {
8
+ class Service extends Container {
8
9
  /**
9
10
  * 构造函数
10
- * @param {Object} knex - Knex数据库实例
11
11
  * @param {string} tableName - 表名
12
+ * @param {string} dbName - 数据库名称,默认使用默认数据库
12
13
  */
13
- constructor(knex = null, tableName = null) {
14
- if (knex && tableName) {
15
- this.db = knex;
14
+ constructor(tableName = null, dbName = null) {
15
+ super('service');
16
+
17
+ // 根据数据库名称获取,否则使用默认数据库
18
+ if (dbName) {
19
+ this.db = Chan.dbManager.get(dbName);
20
+ } else {
21
+ this.db = Chan.db;
22
+ }
23
+
24
+ if (tableName) {
16
25
  this.tableName = tableName;
17
26
  }
18
27
 
19
28
  this._dateFields = ['created_at', 'updated_at', 'deleted_at', 'publish_time', 'start_time', 'end_time', 'login_time', 'created_date', 'updated_date', 'createdAt', 'updatedAt', 'deletedAt', 'publishTime', 'startTime', 'endTime', 'loginTime', 'createdDate', 'updatedDate'];
29
+
30
+ // 自动注册事件监听器(如果子类定义了 on 方法)
31
+ if (typeof this.on === 'function') {
32
+ this.on();
33
+ }
20
34
  }
21
35
 
22
36
  /**
package/helper/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { loaderSort, loadConfig, clearConfigCache, bindInstance, loadController } from "./loader.js";
1
+ export { loaderSort, loadConfig, clearConfigCache, loadController } from "./loader.js";
2
2
  export { formatTime } from "./time.js";
3
3
  export { formatDateFields } from "./time.js";
4
4
  export { default as Cache } from "./cache.js";