@anjianshi/utils 3.0.0 → 3.0.2

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 (164) hide show
  1. package/env-browser/device.d.ts +24 -0
  2. package/env-browser/device.js +50 -0
  3. package/env-browser/global.d.ts +10 -0
  4. package/env-browser/global.js +15 -0
  5. package/env-browser/load-script.d.ts +5 -0
  6. package/env-browser/load-script.js +13 -0
  7. package/env-browser/logging.d.ts +18 -0
  8. package/env-browser/logging.js +49 -0
  9. package/env-browser/manage-vconsole.d.ts +16 -0
  10. package/env-browser/manage-vconsole.js +38 -0
  11. package/env-node/crypto-random.d.ts +13 -0
  12. package/env-node/crypto-random.js +28 -0
  13. package/env-node/fs.d.ts +19 -0
  14. package/env-node/fs.js +48 -0
  15. package/env-node/index.d.ts +5 -0
  16. package/env-node/index.js +5 -0
  17. package/env-node/logging/handlers.d.ts +58 -0
  18. package/env-node/logging/handlers.js +154 -0
  19. package/env-node/logging/index.d.ts +11 -0
  20. package/env-node/logging/index.js +14 -0
  21. package/{src/env-react/emotion-register-globals.ts → env-react/emotion-register-globals.d.ts} +2 -5
  22. package/env-react/emotion-register-globals.js +5 -0
  23. package/env-react/emotion.d.ts +20 -0
  24. package/env-react/emotion.jsx +34 -0
  25. package/env-react/hooks.d.ts +23 -0
  26. package/env-react/hooks.js +47 -0
  27. package/env-react/index.d.ts +1 -0
  28. package/env-react/index.js +1 -0
  29. package/env-react/react-register-globals.d.ts +21 -0
  30. package/env-react/react-register-globals.js +19 -0
  31. package/env-service/controllers.d.ts +30 -0
  32. package/env-service/controllers.js +41 -0
  33. package/env-service/env-reader.d.ts +55 -0
  34. package/env-service/env-reader.js +79 -0
  35. package/env-service/index.d.ts +6 -0
  36. package/env-service/index.js +6 -0
  37. package/env-service/prisma/adapt-logging.d.ts +21 -0
  38. package/env-service/prisma/adapt-logging.js +30 -0
  39. package/env-service/prisma/extensions/exist.d.ts +10 -0
  40. package/env-service/prisma/extensions/exist.js +16 -0
  41. package/env-service/prisma/extensions/find-and-count.d.ts +7 -0
  42. package/env-service/prisma/extensions/find-and-count.js +19 -0
  43. package/env-service/prisma/extensions/soft-delete.d.ts +52 -0
  44. package/env-service/prisma/extensions/soft-delete.js +123 -0
  45. package/env-service/prisma/extensions/with-transaction.d.ts +9 -0
  46. package/env-service/prisma/extensions/with-transaction.js +54 -0
  47. package/env-service/prisma/index.d.ts +6 -0
  48. package/env-service/prisma/index.js +6 -0
  49. package/env-service/prisma/transaction-contexted.d.ts +11 -0
  50. package/env-service/prisma/transaction-contexted.js +52 -0
  51. package/env-service/redis-cache.d.ts +39 -0
  52. package/env-service/redis-cache.js +116 -0
  53. package/env-service/tasks.d.ts +12 -0
  54. package/env-service/tasks.js +37 -0
  55. package/index.d.ts +4 -0
  56. package/index.js +4 -0
  57. package/init-dayjs.d.ts +2 -0
  58. package/init-dayjs.js +7 -0
  59. package/lang/async.d.ts +19 -0
  60. package/lang/async.js +34 -0
  61. package/lang/color.d.ts +37 -0
  62. package/lang/color.js +111 -0
  63. package/lang/index.d.ts +8 -0
  64. package/lang/index.js +8 -0
  65. package/lang/object.d.ts +12 -0
  66. package/lang/object.js +41 -0
  67. package/lang/random.d.ts +13 -0
  68. package/lang/random.js +24 -0
  69. package/lang/result.d.ts +47 -0
  70. package/lang/result.js +45 -0
  71. package/lang/string.d.ts +29 -0
  72. package/lang/string.js +92 -0
  73. package/lang/time.d.ts +10 -0
  74. package/lang/time.js +18 -0
  75. package/{src/lang/types.ts → lang/types.d.ts} +23 -43
  76. package/lang/types.js +28 -0
  77. package/logging/adapt.d.ts +10 -0
  78. package/logging/adapt.js +43 -0
  79. package/logging/formatters.d.ts +10 -0
  80. package/logging/formatters.js +22 -0
  81. package/logging/index.d.ts +45 -0
  82. package/logging/index.js +90 -0
  83. package/md5.d.ts +30 -0
  84. package/md5.js +308 -0
  85. package/package.json +10 -19
  86. package/safe-request.d.ts +53 -0
  87. package/safe-request.js +140 -0
  88. package/url.d.ts +77 -0
  89. package/url.js +149 -0
  90. package/validators/array.d.ts +30 -0
  91. package/validators/array.js +47 -0
  92. package/validators/base.d.ts +82 -0
  93. package/validators/base.js +42 -0
  94. package/validators/boolean.d.ts +3 -0
  95. package/validators/boolean.js +22 -0
  96. package/validators/datetime.d.ts +12 -0
  97. package/validators/datetime.js +30 -0
  98. package/validators/factory.d.ts +70 -0
  99. package/validators/factory.js +121 -0
  100. package/validators/index.d.ts +9 -0
  101. package/validators/index.js +9 -0
  102. package/validators/number.d.ts +19 -0
  103. package/validators/number.js +26 -0
  104. package/validators/object.d.ts +28 -0
  105. package/validators/object.js +49 -0
  106. package/validators/one-of.d.ts +10 -0
  107. package/validators/one-of.js +15 -0
  108. package/validators/string.d.ts +22 -0
  109. package/validators/string.js +35 -0
  110. package/README.md +0 -10
  111. package/eslint.config.cjs +0 -33
  112. package/publish-prepare.cjs +0 -16
  113. package/src/env-browser/device.ts +0 -62
  114. package/src/env-browser/global.ts +0 -21
  115. package/src/env-browser/load-script.ts +0 -13
  116. package/src/env-browser/logging.ts +0 -58
  117. package/src/env-browser/manage-vconsole.ts +0 -54
  118. package/src/env-node/crypto-random.ts +0 -30
  119. package/src/env-node/fs.ts +0 -50
  120. package/src/env-node/index.ts +0 -5
  121. package/src/env-node/logging/handlers.ts +0 -190
  122. package/src/env-node/logging/index.ts +0 -16
  123. package/src/env-react/emotion.tsx +0 -42
  124. package/src/env-react/hooks.ts +0 -59
  125. package/src/env-react/index.ts +0 -1
  126. package/src/env-react/react-register-globals.ts +0 -53
  127. package/src/env-service/controllers.ts +0 -93
  128. package/src/env-service/env-reader.ts +0 -141
  129. package/src/env-service/index.ts +0 -6
  130. package/src/env-service/prisma/adapt-logging.ts +0 -39
  131. package/src/env-service/prisma/extensions/exist.ts +0 -21
  132. package/src/env-service/prisma/extensions/find-and-count.ts +0 -24
  133. package/src/env-service/prisma/extensions/soft-delete.ts +0 -162
  134. package/src/env-service/prisma/extensions/with-transaction.ts +0 -65
  135. package/src/env-service/prisma/index.ts +0 -6
  136. package/src/env-service/prisma/transaction-contexted.ts +0 -80
  137. package/src/env-service/redis-cache.ts +0 -142
  138. package/src/env-service/tasks.ts +0 -45
  139. package/src/index.ts +0 -4
  140. package/src/init-dayjs.ts +0 -8
  141. package/src/lang/async.ts +0 -47
  142. package/src/lang/color.ts +0 -119
  143. package/src/lang/index.ts +0 -8
  144. package/src/lang/object.ts +0 -39
  145. package/src/lang/random.ts +0 -25
  146. package/src/lang/result.ts +0 -78
  147. package/src/lang/string.ts +0 -95
  148. package/src/lang/time.ts +0 -19
  149. package/src/logging/adapt.ts +0 -49
  150. package/src/logging/formatters.ts +0 -23
  151. package/src/logging/index.ts +0 -106
  152. package/src/md5.ts +0 -318
  153. package/src/safe-request.ts +0 -193
  154. package/src/url.ts +0 -185
  155. package/src/validators/array.ts +0 -97
  156. package/src/validators/base.ts +0 -145
  157. package/src/validators/boolean.ts +0 -21
  158. package/src/validators/datetime.ts +0 -39
  159. package/src/validators/factory.ts +0 -244
  160. package/src/validators/index.ts +0 -9
  161. package/src/validators/number.ts +0 -54
  162. package/src/validators/object.ts +0 -101
  163. package/src/validators/one-of.ts +0 -33
  164. package/src/validators/string.ts +0 -72
@@ -0,0 +1,123 @@
1
+ /**
2
+ * 扩展 Prisma 实现软删除
3
+ *
4
+ * 1. 有 deleteTime 字段的 model 支持软删除。
5
+ * 2. 执行 delete() 和 deleteMany() 时默认是进行软删除;可指定 soft 为 false 来彻底删除;执行软删除时可指定要额外更新的 data。
6
+ * 2. 查询时会忽略被软删除的记录;可指定 withDeleted 为 true 来包含它们。
7
+ * 4. 可通过 restore() 和 restoreMany() 恢复软删除的记录。
8
+ *
9
+ * 扩展实现方式参考:
10
+ * https://www.prisma.io/docs/orm/prisma-client/client-extensions/type-utilities#add-a-custom-property-to-a-method
11
+ * https://www.npmjs.com/package/@prisma/extension-accelerate?activeTab=code => @prisma/extension-accelerate/dist/esm/extension.js
12
+ *
13
+ * 此扩展修改了 Prisma 的原生方法。
14
+ * 为保证其他扩展也应用到修改过的这些方法,此扩展应尽可能放在最前面。
15
+ */
16
+ import { Prisma } from '@prisma/client/extension';
17
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
18
+ function getModel(that) {
19
+ const context = Prisma.getExtensionContext(that);
20
+ // 1. 此扩展修改了 Prisma 原生的方法,所以要通过 context.$parent[context.$name] 获取上一层的 model,不然会自己调用自己导致死循环。
21
+ // 2. 如果此扩展后面还应用了其他扩展,那么仅仅一层 $parent 取得的 model 还是这个扩展修改过的版本而不是原生的。
22
+ // 此时需要递归向上,直到取得未经此扩展修改过的 model。不然此扩展的业务逻辑会被重复执行,
23
+ // 而因为第一次执行时已经把定制参数消解掉了,第二次执行时会误以为没有传入定制参数,最终导致定制参数失效。
24
+ let model = context;
25
+ do {
26
+ model = model.$parent[context.$name];
27
+ } while ('withSoftDeleteExtension' in model);
28
+ const supportSoftDelete = 'deleteTime' in model.fields;
29
+ return { model, supportSoftDelete };
30
+ }
31
+ function query(that, inputArgs, method) {
32
+ const { model, supportSoftDelete } = getModel(that);
33
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
34
+ const { withDeleted = false, ...args } = inputArgs;
35
+ return model[method]({
36
+ ...args,
37
+ where: !supportSoftDelete || withDeleted ? args.where : { ...args.where, deleteTime: null },
38
+ });
39
+ }
40
+ export const softDelete = Prisma.defineExtension({
41
+ name: 'softDeleted',
42
+ model: {
43
+ $allModels: {
44
+ withSoftDeleteExtension: true,
45
+ // -----------------------------
46
+ // 操作
47
+ // -----------------------------
48
+ delete(rawArgs) {
49
+ const { model, supportSoftDelete } = getModel(this);
50
+ const { soft = true, data, ...args } = rawArgs;
51
+ if (supportSoftDelete && soft) {
52
+ return model.update({
53
+ ...args, // .delete() 的参数 .update() 也都支持
54
+ data: { ...(data ?? {}), deleteTime: new Date() },
55
+ }); // .update() 的返回值和 .delete() 一样
56
+ }
57
+ else {
58
+ return model.delete(args);
59
+ }
60
+ },
61
+ deleteMany(rawArgs) {
62
+ const { model, supportSoftDelete } = getModel(this);
63
+ const { soft = true, data, ...args } = rawArgs;
64
+ if (supportSoftDelete && soft) {
65
+ return model.updateMany({
66
+ ...args, // .deleteMany() 的参数 .updateMany() 也都支持
67
+ data: { ...(data ?? {}), deleteTime: new Date() },
68
+ }); // .updateMany() 的返回值和 .deleteMany() 一样
69
+ }
70
+ else {
71
+ return model.deleteMany(args);
72
+ }
73
+ },
74
+ restore(rawArgs) {
75
+ const { data, ...args } = rawArgs;
76
+ const { model, supportSoftDelete } = getModel(this);
77
+ if (!supportSoftDelete)
78
+ throw new Error('当前模型不支持软删除,不能执行恢复');
79
+ return model.update({
80
+ ...args,
81
+ data: { ...(data ?? {}), deleteTime: null },
82
+ });
83
+ },
84
+ restoreMany(rawArgs) {
85
+ const { data, ...args } = rawArgs;
86
+ const { model, supportSoftDelete } = getModel(this);
87
+ if (!supportSoftDelete)
88
+ throw new Error('当前模型不支持软删除,不能执行恢复');
89
+ return model.updateMany({
90
+ ...args,
91
+ data: { ...(data ?? {}), deleteTime: new Date() },
92
+ });
93
+ },
94
+ // -----------------------------
95
+ // 查询
96
+ // -----------------------------
97
+ aggregate(inputArgs) {
98
+ return query(this, inputArgs, 'aggregate');
99
+ },
100
+ count(inputArgs) {
101
+ return query(this, inputArgs, 'count');
102
+ },
103
+ findFirst(inputArgs) {
104
+ return query(this, inputArgs, 'findFirst');
105
+ },
106
+ findFirstOrThrow(inputArgs) {
107
+ return query(this, inputArgs, 'findFirstOrThrow');
108
+ },
109
+ findMany(inputArgs) {
110
+ return query(this, inputArgs, 'findMany');
111
+ },
112
+ findUnique(inputArgs) {
113
+ return query(this, inputArgs, 'findUnique');
114
+ },
115
+ findUniqueOrThrow(inputArgs) {
116
+ return query(this, inputArgs, 'findUniqueOrThrow');
117
+ },
118
+ groupBy(inputArgs) {
119
+ return query(this, inputArgs, 'groupBy');
120
+ },
121
+ },
122
+ },
123
+ });
@@ -0,0 +1,9 @@
1
+ import { type ITXClientDenyList } from '@prisma/client/runtime/library.js';
2
+ import type { Result, Failed } from '../../../index.js';
3
+ export declare const withTransaction: (client: any) => import("@prisma/client/extension").PrismaClientExtends<import("@prisma/client/runtime/library").InternalArgs<{}, {}, {}, {
4
+ $withTransaction: typeof $withTransaction;
5
+ }>>;
6
+ export type GetPrismaClientInTransaction<PrismaClient> = Omit<PrismaClient, ITXClientDenyList>;
7
+ export type WithTransactionMethod = typeof $withTransaction;
8
+ declare function $withTransaction<That extends object, R extends Result<unknown, unknown>>(this: That, callback: (dbInTransaction: GetPrismaClientInTransaction<That>) => Promise<R>): Promise<Failed<any> | R>;
9
+ export {};
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 在事务中执行回调。与 $transaction 有几点不同:
3
+ * 1. 回调必须返回 Result 值
4
+ * 2. 回调返回 Failed 值或抛出异常都会触发回滚。
5
+ * 如果是返回 Failed,会作为此方法的返回值;如果是抛出异常,则异常会继续向上传递,直到被捕获或触发请求失败。
6
+ * 3. 如果已经处于事务中,会沿用上层事务,且回调返回 Failed 或抛出异常会触发上层事务的回滚。
7
+ *
8
+ * const result: Result = await db.$withTransaction(
9
+ * async (dbInTransaction) => {
10
+ * // do something
11
+ * return success()
12
+ * }
13
+ * )
14
+ */
15
+ import { Prisma } from '@prisma/client/extension.js';
16
+ export const withTransaction = Prisma.defineExtension({
17
+ name: 'withTransaction',
18
+ client: {
19
+ $withTransaction,
20
+ },
21
+ });
22
+ class FailedInTransaction extends Error {
23
+ failed;
24
+ constructor(failed) {
25
+ super(failed.message);
26
+ this.failed = failed;
27
+ }
28
+ }
29
+ // 注意:此函数的返回值为 `R | Failed<any>`,例如实际可能为 `Result<xxx, xxx> | Failed<any>`,这是有意为之的,`Failed<any>` 并不多余。
30
+ // 因为有时 callback() 只会返回 success 结果,此时 R=Success<xxx>,但是 $withTransaction 整体的返回值仍有可能有 Failed<any>,所以不能用 R 作为整体返回值。
31
+ async function $withTransaction(callback) {
32
+ const executeCallback = async (dbInTransaction) => {
33
+ const result = await callback(dbInTransaction);
34
+ if (result.success)
35
+ return result;
36
+ else
37
+ throw new FailedInTransaction(result);
38
+ };
39
+ if ('$transaction' in this && this.$transaction) {
40
+ // 如果当前不在事务中,开启新事务并执行回调
41
+ try {
42
+ return await this.$transaction(async (dbInTransaction) => executeCallback(dbInTransaction));
43
+ }
44
+ catch (e) {
45
+ if (e instanceof FailedInTransaction)
46
+ return e.failed;
47
+ throw e;
48
+ }
49
+ }
50
+ else {
51
+ // 已经在事务中,直接执行回调(如果有异常,上层开启事务的代码会捕获)
52
+ return executeCallback(this);
53
+ }
54
+ }
@@ -0,0 +1,6 @@
1
+ export * from './extensions/exist.js';
2
+ export * from './extensions/find-and-count.js';
3
+ export * from './extensions/soft-delete.js';
4
+ export * from './extensions/with-transaction.js';
5
+ export * from './transaction-contexted.js';
6
+ export * from './adapt-logging.js';
@@ -0,0 +1,6 @@
1
+ export * from './extensions/exist.js';
2
+ export * from './extensions/find-and-count.js';
3
+ export * from './extensions/soft-delete.js';
4
+ export * from './extensions/with-transaction.js';
5
+ export * from './transaction-contexted.js';
6
+ export * from './adapt-logging.js';
@@ -0,0 +1,11 @@
1
+ import type { WithTransactionMethod } from './extensions/with-transaction.js';
2
+ /**
3
+ * 返回一个可以在事务内和事务外通用的 PrismaClient 代理对象。
4
+ * 当前在事务内时,调用它是调用事务内的 client;当前不在事务内时,调用它是调用全局 client。
5
+ * 这样当一个事务涉及多个函数调用时,就不用把事务内 client 传递来传递去了。
6
+ * 注意:应给每个线性流程(例如一个请求)单独生成一个此对象,不能作为全局对象使用,不然可能出现事务冲突。
7
+ */
8
+ export declare function getTransactionContextedPrismaClient<AppPrismaClient extends {
9
+ $transaction: (...args: any[]) => Promise<unknown>;
10
+ $withTransaction: WithTransactionMethod;
11
+ }>(prisma: AppPrismaClient): AppPrismaClient;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 返回一个可以在事务内和事务外通用的 PrismaClient 代理对象。
3
+ * 当前在事务内时,调用它是调用事务内的 client;当前不在事务内时,调用它是调用全局 client。
4
+ * 这样当一个事务涉及多个函数调用时,就不用把事务内 client 传递来传递去了。
5
+ * 注意:应给每个线性流程(例如一个请求)单独生成一个此对象,不能作为全局对象使用,不然可能出现事务冲突。
6
+ */
7
+ export function getTransactionContextedPrismaClient(prisma) {
8
+ let client = prisma;
9
+ async function callCallbackInTransaction(callback, clientInTransaction) {
10
+ const prevClient = client;
11
+ const currentClient = clientInTransaction;
12
+ client = currentClient;
13
+ function restoreClient() {
14
+ if (client !== currentClient)
15
+ throw new Error('事务冲突,必须等一个事务结束后再开启另一个事务');
16
+ client = prevClient;
17
+ }
18
+ try {
19
+ const res = await callback(client);
20
+ restoreClient();
21
+ return res;
22
+ }
23
+ catch (e) {
24
+ restoreClient();
25
+ throw e;
26
+ }
27
+ }
28
+ async function $transaction(arg, ...restArgs) {
29
+ if (typeof arg === 'function') {
30
+ const wrappedCallback = callCallbackInTransaction.bind(null, arg);
31
+ return client.$transaction(wrappedCallback, ...restArgs);
32
+ }
33
+ else {
34
+ return client.$transaction(arg, ...restArgs);
35
+ }
36
+ }
37
+ async function $withTransaction(callback) {
38
+ return client.$withTransaction(async (clientInTransaction) => callCallbackInTransaction(callback, clientInTransaction));
39
+ }
40
+ return new Proxy({}, {
41
+ has(_, prop) {
42
+ return prop in client;
43
+ },
44
+ get(_, prop) {
45
+ if (prop === '$transaction')
46
+ return $transaction;
47
+ if (prop === '$withTransaction')
48
+ return $withTransaction;
49
+ return client[prop];
50
+ },
51
+ });
52
+ }
@@ -0,0 +1,39 @@
1
+ import { type RedisClientType } from 'redis';
2
+ import { type Logger } from '../logging/index.js';
3
+ export declare function initRedisLogging(redis: RedisClientType, logger?: Logger): void;
4
+ export interface CacheOptions {
5
+ logger: Logger;
6
+ /** 数据有效期,单位秒。默认为 10 分钟。小于等于 0 代表不设有效期 */
7
+ expires: number;
8
+ /** 读取时是否自动刷新有效期,仅设置了 expire 时有效,默认为 true */
9
+ refreshOnRead: boolean;
10
+ /** 若为 true,读取数据后会立即将其删除,默认为 false */
11
+ oneTime: boolean;
12
+ }
13
+ /**
14
+ * 维护缓存数据
15
+ * 1. 每个 Cache 实例只维护一个主题的数据,且需明确定义数据类型,这样设计可明确对每一项缓存的使用、避免混乱。
16
+ * 2. 值在存储时会 JSON 化,读取时再进行 JSON 解析(支持 JSON 化 Date 对象)。
17
+ */
18
+ export declare class Cache<T> {
19
+ readonly redis: RedisClientType;
20
+ readonly topic: string;
21
+ readonly options: CacheOptions;
22
+ constructor(redis: RedisClientType, topic: string, options?: Partial<CacheOptions>);
23
+ get logger(): Logger;
24
+ protected jsonStringify(value: T): string;
25
+ protected jsonParse(redisValue: string): T;
26
+ protected getRedisKey(identity: string): string;
27
+ /** 读取一项内容 */
28
+ get(identity: string, defaults: T): Promise<T>;
29
+ get(identity?: string): Promise<T | undefined>;
30
+ /** 写入/更新一项内容 */
31
+ set(value: T): Promise<void>;
32
+ set(identity: string, value: T): Promise<void>;
33
+ /** 移除一项内容 */
34
+ delete(identity?: string | string[]): Promise<number>;
35
+ /** 刷新一项内容的过期时间 */
36
+ refresh(identity?: string): Promise<number | false>;
37
+ /** 确认一项内容是否存在 */
38
+ exists(identity?: string): Promise<boolean>;
39
+ }
@@ -0,0 +1,116 @@
1
+ import { logger as rootLogger } from '../logging/index.js';
2
+ export function initRedisLogging(redis, logger) {
3
+ logger ??= rootLogger.getChild('redis');
4
+ redis.on('connect', () => logger.info('connecting'));
5
+ redis.on('ready', () => logger.info('connected'));
6
+ redis.on('end', () => logger.info('connection closed'));
7
+ redis.on('reconnecting', () => logger.info('reconnecting'));
8
+ redis.on('error', error => logger.error(error));
9
+ }
10
+ /**
11
+ * 维护缓存数据
12
+ * 1. 每个 Cache 实例只维护一个主题的数据,且需明确定义数据类型,这样设计可明确对每一项缓存的使用、避免混乱。
13
+ * 2. 值在存储时会 JSON 化,读取时再进行 JSON 解析(支持 JSON 化 Date 对象)。
14
+ */
15
+ export class Cache {
16
+ redis;
17
+ topic;
18
+ options;
19
+ constructor(redis, topic, options) {
20
+ this.redis = redis;
21
+ this.topic = topic;
22
+ this.options = {
23
+ logger: rootLogger.getChild('cache'),
24
+ expires: 600,
25
+ refreshOnRead: true,
26
+ oneTime: false,
27
+ ...(options ?? {}),
28
+ };
29
+ }
30
+ get logger() {
31
+ return this.options.logger;
32
+ }
33
+ // 经过定制的 JSON 序列化和解析方法
34
+ jsonStringify(value) {
35
+ // 参考:https://stackoverflow.com/a/54037861/2815178
36
+ function replacer(key, value) {
37
+ // value 是经过预处理过的值,对于 Date 对象,此时已经是 string,需要通过 this[key] 才能拿到 Date 值。
38
+ const rawValue = this[key];
39
+ if (rawValue instanceof Date)
40
+ return { __json_type: 'date', value: rawValue.toISOString() };
41
+ return value;
42
+ }
43
+ return JSON.stringify(value, replacer);
44
+ }
45
+ jsonParse(redisValue) {
46
+ function reviver(key, value) {
47
+ if (typeof value === 'object' && value !== null) {
48
+ const obj = value;
49
+ if (obj.__json_type === 'date')
50
+ return new Date(obj.value);
51
+ }
52
+ return value;
53
+ }
54
+ return JSON.parse(redisValue, reviver);
55
+ }
56
+ getRedisKey(identity) {
57
+ return `${this.topic}${identity ? ':' + identity : ''}`;
58
+ }
59
+ async get(identity = '', defaults) {
60
+ const redisKey = this.getRedisKey(identity);
61
+ const redisValue = await this.redis.get(redisKey);
62
+ this.logger.debug('get', redisKey, redisValue);
63
+ if (redisValue === null)
64
+ return defaults;
65
+ if (this.options.refreshOnRead)
66
+ void this.refresh(identity);
67
+ try {
68
+ const value = this.jsonParse(redisValue);
69
+ if (this.options.oneTime)
70
+ await this.delete(identity);
71
+ return value;
72
+ }
73
+ catch (error) {
74
+ this.logger.error(`解析 cache 数据失败,key=${redisKey},value=${redisValue}`, error);
75
+ void this.delete(identity);
76
+ return defaults;
77
+ }
78
+ }
79
+ async set(identity, value) {
80
+ if (value === undefined) {
81
+ value = identity;
82
+ identity = '';
83
+ }
84
+ else {
85
+ identity = identity;
86
+ }
87
+ const redisKey = this.getRedisKey(identity);
88
+ let redisValue;
89
+ try {
90
+ redisValue = this.jsonStringify(value);
91
+ }
92
+ catch (error) {
93
+ this.logger.error(`格式化 cache 数据失败,key=${redisKey}`, value, error);
94
+ throw error;
95
+ }
96
+ this.logger.debug('set', redisKey, redisValue);
97
+ await this.redis.set(redisKey, redisValue, { EX: this.options.expires });
98
+ }
99
+ /** 移除一项内容 */
100
+ async delete(identity = '') {
101
+ const identities = Array.isArray(identity) ? identity : [identity];
102
+ this.logger.debug('delete', identities);
103
+ return this.redis.del(identities.map(identity => this.getRedisKey(identity)));
104
+ }
105
+ /** 刷新一项内容的过期时间 */
106
+ async refresh(identity = '') {
107
+ if (this.options.expires >= 0)
108
+ return this.redis.expire(this.getRedisKey(identity), this.options.expires);
109
+ else
110
+ return false;
111
+ }
112
+ /** 确认一项内容是否存在 */
113
+ async exists(identity = '') {
114
+ return (await this.redis.exists(this.getRedisKey(identity))) === 1;
115
+ }
116
+ }
@@ -0,0 +1,12 @@
1
+ import { type Logger } from '../logging/index.js';
2
+ /** 返回 false 可结束任务 */
3
+ export type TaskExecutor<Context> = (context: Context, logger: Logger) => Promise<void> | Promise<undefined | boolean>;
4
+ /**
5
+ * 执行定期任务
6
+ */
7
+ export declare abstract class TaskManager<Context> {
8
+ protected baseLogger: Logger;
9
+ constructor(baseLogger?: Logger);
10
+ abstract getContext(taskName: string): Context;
11
+ run(name: string, interval: number, executor: TaskExecutor<Context>): Promise<void>;
12
+ }
@@ -0,0 +1,37 @@
1
+ import { sleep } from '../lang/async.js';
2
+ import { logger as rootLogger } from '../logging/index.js';
3
+ /**
4
+ * 执行定期任务
5
+ */
6
+ export class TaskManager {
7
+ baseLogger;
8
+ constructor(baseLogger = rootLogger.getChild('task')) {
9
+ this.baseLogger = baseLogger;
10
+ }
11
+ async run(name, interval, executor) {
12
+ await sleep(1000);
13
+ const logger = this.baseLogger.getChild(name);
14
+ let nextId = 1;
15
+ while (true) {
16
+ const id = nextId++;
17
+ if (id >= Number.MAX_SAFE_INTEGER)
18
+ nextId = 1;
19
+ const start = Date.now();
20
+ logger.info(`#${id} 任务开始`);
21
+ try {
22
+ const context = this.getContext(name);
23
+ const result = await executor(context, logger);
24
+ const cost = (Date.now() - start) / 1000;
25
+ logger.info(`#${id} 任务完成,耗时 ${cost}s`);
26
+ if (result === false) {
27
+ logger.info('任务结束');
28
+ break;
29
+ }
30
+ }
31
+ catch (err) {
32
+ logger.error(`#${id} 任务失败`, err);
33
+ }
34
+ await sleep(interval);
35
+ }
36
+ }
37
+ }
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './lang/index.js';
2
+ export * from './url.js';
3
+ export * from './safe-request.js';
4
+ export * from './logging/index.js';
package/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './lang/index.js';
2
+ export * from './url.js';
3
+ export * from './safe-request.js';
4
+ export * from './logging/index.js';
@@ -0,0 +1,2 @@
1
+ import 'dayjs/locale/zh-cn.js';
2
+ export declare function initDayJs(): void;
package/init-dayjs.js ADDED
@@ -0,0 +1,7 @@
1
+ import dayjs from 'dayjs';
2
+ import objectSupport from 'dayjs/plugin/objectSupport.js';
3
+ import 'dayjs/locale/zh-cn.js';
4
+ export function initDayJs() {
5
+ dayjs.extend(objectSupport);
6
+ dayjs.locale('zh-cn');
7
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 异步行为相关函数
3
+ */
4
+ /**
5
+ * 返回一个指定毫秒后 resolve 的 Promise
6
+ * 若指定了 resolveValue,则 Promise 最终会解析出此值
7
+ */
8
+ declare function sleep(ms: number): Promise<undefined>;
9
+ declare function sleep<T>(ms: number, resolveValue: T): Promise<T>;
10
+ export { sleep };
11
+ /**
12
+ * 给 Promise 增加时限。
13
+ * - 若 silent 为 false,超时时 reject 一个 TimeoutError
14
+ * - 若 silent 为 true,则超时后,promise 永远不会 resolve 或 reject
15
+ */
16
+ export declare function timeout<T>(promise: Promise<T>, timeoutMS: number, silent?: boolean): Promise<T>;
17
+ export declare class TimeoutError extends Error {
18
+ isTimeout: boolean;
19
+ }
package/lang/async.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 异步行为相关函数
3
+ */
4
+ async function sleep(ms, resolveValue) {
5
+ return new Promise(resolve => {
6
+ setTimeout(() => resolve(resolveValue), ms);
7
+ });
8
+ }
9
+ export { sleep };
10
+ /**
11
+ * 给 Promise 增加时限。
12
+ * - 若 silent 为 false,超时时 reject 一个 TimeoutError
13
+ * - 若 silent 为 true,则超时后,promise 永远不会 resolve 或 reject
14
+ */
15
+ export async function timeout(promise, timeoutMS, silent = false) {
16
+ return new Promise((resolve, reject) => {
17
+ let isTimeout = false;
18
+ setTimeout(() => {
19
+ isTimeout = true;
20
+ if (!silent)
21
+ reject(new TimeoutError('timeout'));
22
+ }, timeoutMS);
23
+ promise.then(result => {
24
+ if (!isTimeout)
25
+ resolve(result);
26
+ }, (error) => {
27
+ if (!isTimeout)
28
+ reject(error);
29
+ });
30
+ });
31
+ }
32
+ export class TimeoutError extends Error {
33
+ isTimeout = true;
34
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 计算颜色数值
3
+ */
4
+ /**
5
+ * hex 颜色字符串转换为 rgba 数值
6
+ * 例如 '#22334455' => [34,51,68,0.33]
7
+ *
8
+ * hex 字符串支持以下格式:
9
+ * - 带/不带开头的 #
10
+ * - #RGB
11
+ * - #RGBA
12
+ * - #RRGGBB
13
+ * - #RRGGBBAA
14
+ */
15
+ export declare function hex2rgba(rawHex: string): readonly [number, number, number, number];
16
+ /**
17
+ * 把 rgba 数组转换为 hex 颜色字符串
18
+ * 如果 alpha 部分为 1,会将其省略
19
+ * [34,51,68,0.33] => '#22334455'
20
+ */
21
+ export declare function rgba2hex(rgba: [number, number, number, number]): string;
22
+ /**
23
+ * rgb 转成 hsl 值(以便计算亮度)
24
+ * https://www.rapidtables.com/convert/color/rgb-to-hsl.html
25
+ */
26
+ export declare function rgb2hsl([r, g, b]: [number, number, number]): readonly [number, number, number];
27
+ /**
28
+ * hsl 转成 rgb
29
+ * https://www.rapidtables.com/convert/color/hsl-to-rgb.html
30
+ */
31
+ export declare function hsl2rgb([h, s, l]: [number, number, number]): readonly [number, number, number];
32
+ /**
33
+ * 调整颜色深浅
34
+ * lightness('#111', 0.2) => lighten(20%)
35
+ * lightness('#111', -0.5) => darken(50%)
36
+ */
37
+ export declare function lightness(hex: string, ratio: number): string;