@greypan/js-kit 1.0.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 (83) hide show
  1. package/.turbo/turbo-build.log +38 -0
  2. package/.turbo/turbo-test.log +87 -0
  3. package/README.CN.md +5 -0
  4. package/README.md +5 -0
  5. package/dist/_virtual/_rolldown/runtime.js +11 -0
  6. package/dist/fetch/compose.d.ts +3 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +10 -0
  9. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/funnel.js +40 -0
  10. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/funnel.js.map +1 -0
  11. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/isNullish.js +8 -0
  12. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/isNullish.js.map +1 -0
  13. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/lazyDataLastImpl--3B10z3s.js +12 -0
  14. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/lazyDataLastImpl--3B10z3s.js.map +1 -0
  15. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/omitBy.js +14 -0
  16. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/omitBy.js.map +1 -0
  17. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/purry.js +12 -0
  18. package/dist/node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/purry.js.map +1 -0
  19. package/dist/number/index.d.ts +14 -0
  20. package/dist/number/index.js +13 -0
  21. package/dist/number/index.js.map +1 -0
  22. package/dist/paradigm/go.d.ts +12 -0
  23. package/dist/paradigm/go.js +37 -0
  24. package/dist/paradigm/go.js.map +1 -0
  25. package/dist/paradigm/index.d.ts +2 -0
  26. package/dist/paradigm/rust.d.ts +25 -0
  27. package/dist/paradigm/rust.js +42 -0
  28. package/dist/paradigm/rust.js.map +1 -0
  29. package/dist/plugin/batching-emitter.d.ts +4 -0
  30. package/dist/plugin/batching-emitter.js +33 -0
  31. package/dist/plugin/batching-emitter.js.map +1 -0
  32. package/dist/plugin/core.d.ts +10 -0
  33. package/dist/plugin/core.js +23 -0
  34. package/dist/plugin/core.js.map +1 -0
  35. package/dist/plugin/event-emitter.d.ts +4 -0
  36. package/dist/plugin/index.d.ts +2 -0
  37. package/dist/random/index.d.ts +23 -0
  38. package/dist/random/index.js +18 -0
  39. package/dist/random/index.js.map +1 -0
  40. package/dist/timer/controllable-interval.d.ts +34 -0
  41. package/dist/timer/controllable-interval.js +41 -0
  42. package/dist/timer/controllable-interval.js.map +1 -0
  43. package/dist/timer/debounce.d.ts +21 -0
  44. package/dist/timer/debounce.js +30 -0
  45. package/dist/timer/debounce.js.map +1 -0
  46. package/dist/timer/index.d.ts +2 -0
  47. package/dist/url/index.d.ts +9 -0
  48. package/dist/url/index.js +28 -0
  49. package/dist/url/index.js.map +1 -0
  50. package/dist/utility-types/index.d.ts +13 -0
  51. package/package.json +54 -0
  52. package/src/fetch/__tests__/compose.spec.ts +105 -0
  53. package/src/fetch/compose.ts +23 -0
  54. package/src/index.ts +7 -0
  55. package/src/number/__tests__/number.spec.ts +50 -0
  56. package/src/number/index.ts +22 -0
  57. package/src/paradigm/__tests__/go.spec.ts +63 -0
  58. package/src/paradigm/__tests__/rust.spec.ts +68 -0
  59. package/src/paradigm/go.ts +38 -0
  60. package/src/paradigm/index.ts +2 -0
  61. package/src/paradigm/rust.ts +50 -0
  62. package/src/plugin/__tests__/batch-emitter.spec.ts +48 -0
  63. package/src/plugin/__tests__/core.spec.ts +110 -0
  64. package/src/plugin/__tests__/event-emiiter.spec.ts +41 -0
  65. package/src/plugin/batching-emitter.ts +61 -0
  66. package/src/plugin/core.ts +37 -0
  67. package/src/plugin/event-emitter.ts +25 -0
  68. package/src/plugin/index.ts +2 -0
  69. package/src/random/__tests__/random.spec.ts +77 -0
  70. package/src/random/index.ts +37 -0
  71. package/src/timer/__tests__/controllable-interval.spec.ts +51 -0
  72. package/src/timer/controllable-interval.ts +86 -0
  73. package/src/timer/debounce.ts +92 -0
  74. package/src/timer/index.ts +2 -0
  75. package/src/url/README.md +0 -0
  76. package/src/url/__tests__/url.spec.ts +152 -0
  77. package/src/url/index.ts +44 -0
  78. package/src/utility-types/index.ts +19 -0
  79. package/tsconfig.app.json +11 -0
  80. package/tsconfig.json +8 -0
  81. package/tsconfig.node.json +18 -0
  82. package/tsconfig.vitest.json +10 -0
  83. package/vite.config.ts +32 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @file timer
3
+ * @description 增强型定时器工具(ControllableInterval)
4
+ * 特性:
5
+ * 1.进度保留:暂停 (pause) 时记录剩余时间,恢复 (resume) 时补全当前周期,而非重新计时
6
+ * 2.状态可控:支持随时 start、pause、resume、stop
7
+ * 3.链路稳健:基于递归 setTimeout 实现,避免原生 setInterval 在回调耗时过长时的“堆积”效应
8
+ * 4.环境兼容:适配浏览器与 Node.js (SSR),自动处理 Timeout 类型差异
9
+ * @example
10
+ * const timer = createControllableInterval(() => {
11
+ * console.log('tick')
12
+ * }, 1000))
13
+ * timer.start()
14
+ * setTimeout(() => {
15
+ * timer.pause() // 运行 800ms 后暂停,剩余 200ms
16
+ * setTimeout(() => {
17
+ * timer.resume() // 200ms 后立即触发回调,随后回归 1000ms 周期
18
+ * }, 500)
19
+ * }, 800)
20
+ */
21
+ export declare function defineControllableInterval(callback: () => void, interval: number): import('../plugin').Plugin<{
22
+ tick: (delay: number) => void;
23
+ start: () => void;
24
+ pause: () => void;
25
+ resume: () => void;
26
+ stop: () => void;
27
+ }, object>;
28
+ export declare function createControllableInterval(callback: () => void, interval: number): object & {
29
+ tick: (delay: number) => void;
30
+ start: () => void;
31
+ pause: () => void;
32
+ resume: () => void;
33
+ stop: () => void;
34
+ };
@@ -0,0 +1,41 @@
1
+ import { definePlugin as e } from "../plugin/core.js";
2
+ //#region src/timer/controllable-interval.ts
3
+ function t(t, n) {
4
+ return e(() => {
5
+ let e = null, r = !1, i = 0, a = 0;
6
+ function o(i) {
7
+ a = Date.now(), e = setTimeout(() => {
8
+ t(), r || o(n);
9
+ }, i);
10
+ }
11
+ function s() {
12
+ e || r || o(n);
13
+ }
14
+ function c() {
15
+ if (r || !e) return;
16
+ r = !0, clearTimeout(e), e = null;
17
+ let t = Date.now() - a;
18
+ i = Math.max(0, n - t);
19
+ }
20
+ function l() {
21
+ r && (r = !1, o(i || n));
22
+ }
23
+ function u() {
24
+ e && clearTimeout(e), e = null, r = !1, i = 0;
25
+ }
26
+ return {
27
+ tick: o,
28
+ start: s,
29
+ pause: c,
30
+ resume: l,
31
+ stop: u
32
+ };
33
+ });
34
+ }
35
+ function n(e, n) {
36
+ return t(e, n).make();
37
+ }
38
+ //#endregion
39
+ export { n as createControllableInterval, t as defineControllableInterval };
40
+
41
+ //# sourceMappingURL=controllable-interval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controllable-interval.js","names":[],"sources":["../../src/timer/controllable-interval.ts"],"sourcesContent":["/**\n * @file timer\n * @description 增强型定时器工具(ControllableInterval)\n * 特性:\n * 1.进度保留:暂停 (pause) 时记录剩余时间,恢复 (resume) 时补全当前周期,而非重新计时\n * 2.状态可控:支持随时 start、pause、resume、stop\n * 3.链路稳健:基于递归 setTimeout 实现,避免原生 setInterval 在回调耗时过长时的“堆积”效应\n * 4.环境兼容:适配浏览器与 Node.js (SSR),自动处理 Timeout 类型差异\n * @example\n * const timer = createControllableInterval(() => {\n * console.log('tick')\n * }, 1000))\n * timer.start()\n * setTimeout(() => {\n * timer.pause() // 运行 800ms 后暂停,剩余 200ms\n * setTimeout(() => {\n * timer.resume() // 200ms 后立即触发回调,随后回归 1000ms 周期\n * }, 500)\n * }, 800)\n */\n\nimport { definePlugin } from '@/plugin'\n\nexport function defineControllableInterval(callback: () => void, interval: number) {\n return definePlugin(() => {\n let timerId: ReturnType<typeof setTimeout> | null = null\n let isPaused = false\n let remainingTime = 0\n let lastStartTime = 0\n\n function tick(delay: number) {\n lastStartTime = Date.now()\n timerId = setTimeout(() => {\n callback()\n // 只有在没被暂停的情况下,才继续下一次循环\n if (!isPaused) tick(interval)\n }, delay)\n }\n\n function start() {\n if (timerId || isPaused) return\n\n tick(interval)\n }\n\n function pause() {\n if (isPaused || !timerId) return\n\n isPaused = true\n clearTimeout(timerId)\n timerId = null\n\n // 计算当前这一轮还剩多少时间没跑完\n const diff = Date.now() - lastStartTime\n remainingTime = Math.max(0, interval - diff)\n }\n\n function resume() {\n if (!isPaused) return\n\n isPaused = false\n // 恢复执行:先跑完上一轮剩下的时间\n tick(remainingTime || interval)\n }\n\n function stop() {\n if (timerId) clearTimeout(timerId)\n\n timerId = null\n isPaused = false\n remainingTime = 0\n }\n\n return {\n tick,\n start,\n pause,\n resume,\n stop\n }\n })\n}\n\nexport function createControllableInterval(callback: () => void, interval: number) {\n return defineControllableInterval(callback, interval).make()\n}\n"],"mappings":";;AAuBA,SAAgB,EAA2B,GAAsB,GAAkB;CACjF,OAAO,QAAmB;EACxB,IAAI,IAAgD,MAChD,IAAW,IACX,IAAgB,GAChB,IAAgB;EAEpB,SAAS,EAAK,GAAe;GAE3B,AADA,IAAgB,KAAK,IAAI,GACzB,IAAU,iBAAiB;IAGzB,AAFA,EAAS,GAEJ,KAAU,EAAK,CAAQ;GAC9B,GAAG,CAAK;EACV;EAEA,SAAS,IAAQ;GACX,KAAW,KAEf,EAAK,CAAQ;EACf;EAEA,SAAS,IAAQ;GACf,IAAI,KAAY,CAAC,GAAS;GAI1B,AAFA,IAAW,IACX,aAAa,CAAO,GACpB,IAAU;GAGV,IAAM,IAAO,KAAK,IAAI,IAAI;GAC1B,IAAgB,KAAK,IAAI,GAAG,IAAW,CAAI;EAC7C;EAEA,SAAS,IAAS;GACX,MAEL,IAAW,IAEX,EAAK,KAAiB,CAAQ;EAChC;EAEA,SAAS,IAAO;GAKd,AAJI,KAAS,aAAa,CAAO,GAEjC,IAAU,MACV,IAAW,IACX,IAAgB;EAClB;EAEA,OAAO;GACL;GACA;GACA;GACA;GACA;EACF;CACF,CAAC;AACH;AAEA,SAAgB,EAA2B,GAAsB,GAAkB;CACjF,OAAO,EAA2B,GAAU,CAAQ,CAAC,CAAC,KAAK;AAC7D"}
@@ -0,0 +1,21 @@
1
+ type StrictFunction = (...args: never) => unknown;
2
+ type Debouncer<F extends StrictFunction, IsNullable extends boolean = true> = {
3
+ readonly call: (...args: Parameters<F>) => ReturnType<F> | (true extends IsNullable ? undefined : never);
4
+ readonly cancel: () => void;
5
+ readonly flush: () => ReturnType<F> | undefined;
6
+ readonly isPending: boolean;
7
+ readonly cachedValue: ReturnType<F> | undefined;
8
+ };
9
+ type DebounceOptions = {
10
+ readonly waitMs?: number;
11
+ readonly maxWaitMs?: number;
12
+ };
13
+ export declare function debounce<F extends StrictFunction>(func: F, options: DebounceOptions & {
14
+ readonly timing?: 'trailing';
15
+ }): Debouncer<F>;
16
+ export declare function debounce<F extends StrictFunction>(func: F, options: (DebounceOptions & {
17
+ readonly timing: 'both';
18
+ }) | (Omit<DebounceOptions, 'maxWaitMs'> & {
19
+ readonly timing: 'leading';
20
+ })): Debouncer<F, false>;
21
+ export {};
@@ -0,0 +1,30 @@
1
+ import { funnel as e } from "../node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/funnel.js";
2
+ //#region src/timer/debounce.ts
3
+ function t(t, { timing: n, waitMs: r, maxWaitMs: i }) {
4
+ if (i !== void 0 && r !== void 0 && i < r) throw Error(`debounce: maxWaitMs (${i.toString()}) cannot be less than waitMs (${r.toString()})`);
5
+ let a, o = e((e) => {
6
+ a = t(...e);
7
+ }, {
8
+ reducer: (e, ...t) => t,
9
+ minQuietPeriodMs: r ?? i ?? 0,
10
+ ...i !== void 0 && { maxBurstDurationMs: i },
11
+ ...n === "leading" ? { triggerAt: "start" } : n === "both" ? { triggerAt: "both" } : { triggerAt: "end" }
12
+ });
13
+ return {
14
+ call: (...e) => (o.call(...e), a),
15
+ flush: () => (o.flush(), a),
16
+ cancel: () => {
17
+ o.cancel();
18
+ },
19
+ get isPending() {
20
+ return !o.isIdle;
21
+ },
22
+ get cachedValue() {
23
+ return a;
24
+ }
25
+ };
26
+ }
27
+ //#endregion
28
+ export { t as debounce };
29
+
30
+ //# sourceMappingURL=debounce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debounce.js","names":[],"sources":["../../src/timer/debounce.ts"],"sourcesContent":["/**\n * @description remeda debounce 废弃了,这是 remeda 官方提供的替代封装\n * @link https://github.com/remeda/remeda/blob/main/packages/remeda/src/funnel.remeda-debounce.test.ts#L10\n */\nimport { funnel } from 'remeda'\n\ntype StrictFunction = (...args: never) => unknown\ntype Debouncer<F extends StrictFunction, IsNullable extends boolean = true> = {\n readonly call: (...args: Parameters<F>) => ReturnType<F> | (true extends IsNullable ? undefined : never)\n readonly cancel: () => void\n readonly flush: () => ReturnType<F> | undefined\n readonly isPending: boolean\n readonly cachedValue: ReturnType<F> | undefined\n}\n\ntype DebounceOptions = {\n readonly waitMs?: number\n readonly maxWaitMs?: number\n}\n\nexport function debounce<F extends StrictFunction>(\n func: F,\n options: DebounceOptions & { readonly timing?: 'trailing' }\n): Debouncer<F>\nexport function debounce<F extends StrictFunction>(\n func: F,\n options:\n | (DebounceOptions & { readonly timing: 'both' })\n | (Omit<DebounceOptions, 'maxWaitMs'> & { readonly timing: 'leading' })\n): Debouncer<F, false /* call CAN'T return null */>\nexport function debounce<F extends StrictFunction>(\n func: F,\n {\n timing,\n waitMs,\n maxWaitMs\n }: DebounceOptions & {\n readonly timing?: 'both' | 'leading' | 'trailing'\n }\n) {\n if (maxWaitMs !== undefined && waitMs !== undefined && maxWaitMs < waitMs) {\n throw new Error(`debounce: maxWaitMs (${maxWaitMs.toString()}) cannot be less than waitMs (${waitMs.toString()})`)\n }\n\n let cachedValue: ReturnType<F> | undefined\n\n const debouncingFunnel = funnel(\n (args: Parameters<F>) => {\n // Every time the function is invoked the cached value is updated.\n // @ts-expect-error [ts2345, ts2322] -- TypeScript infers the generic sub-\n // types too eagerly, making itself blind to the fact that the types\n // match here.\n cachedValue = func(...args)\n },\n {\n // Debounce stores the latest args it was called with for the next\n // invocation of the callback.\n reducer: (_, ...args: Parameters<F>) => args,\n minQuietPeriodMs: waitMs ?? maxWaitMs ?? 0,\n ...(maxWaitMs !== undefined && { maxBurstDurationMs: maxWaitMs }),\n ...(timing === 'leading'\n ? { triggerAt: 'start' }\n : timing === 'both'\n ? { triggerAt: 'both' }\n : { triggerAt: 'end' })\n }\n )\n\n return {\n call: (...args: Parameters<F>) => {\n debouncingFunnel.call(...args)\n return cachedValue\n },\n\n flush: () => {\n debouncingFunnel.flush()\n return cachedValue\n },\n\n cancel: () => {\n debouncingFunnel.cancel()\n },\n\n get isPending() {\n return !debouncingFunnel.isIdle\n },\n\n get cachedValue() {\n return cachedValue\n }\n }\n}\n"],"mappings":";;AA8BA,SAAgB,EACd,GACA,EACE,WACA,WACA,gBAIF;CACA,IAAI,MAAc,KAAA,KAAa,MAAW,KAAA,KAAa,IAAY,GACjE,MAAU,MAAM,wBAAwB,EAAU,SAAS,EAAE,gCAAgC,EAAO,SAAS,EAAE,EAAE;CAGnH,IAAI,GAEE,IAAmB,GACtB,MAAwB;EAKvB,IAAc,EAAK,GAAG,CAAI;CAC5B,GACA;EAGE,UAAU,GAAG,GAAG,MAAwB;EACxC,kBAAkB,KAAU,KAAa;EACzC,GAAI,MAAc,KAAA,KAAa,EAAE,oBAAoB,EAAU;EAC/D,GAAI,MAAW,YACX,EAAE,WAAW,QAAQ,IACrB,MAAW,SACT,EAAE,WAAW,OAAO,IACpB,EAAE,WAAW,MAAM;CAC3B,CACF;CAEA,OAAO;EACL,OAAO,GAAG,OACR,EAAiB,KAAK,GAAG,CAAI,GACtB;EAGT,cACE,EAAiB,MAAM,GAChB;EAGT,cAAc;GACZ,EAAiB,OAAO;EAC1B;EAEA,IAAI,YAAY;GACd,OAAO,CAAC,EAAiB;EAC3B;EAEA,IAAI,cAAc;GAChB,OAAO;EACT;CACF;AACF"}
@@ -0,0 +1,2 @@
1
+ export * from './controllable-interval';
2
+ export * from './debounce';
@@ -0,0 +1,9 @@
1
+ type QueryValue = string | number | boolean | undefined | null;
2
+ interface URLObject {
3
+ base: string;
4
+ query: Record<string, QueryValue>;
5
+ hash: string;
6
+ }
7
+ export declare function urlParse(url?: string): URLObject;
8
+ export declare function urlStringify(opts: Partial<URLObject>, omitNil?: boolean): string;
9
+ export {};
@@ -0,0 +1,28 @@
1
+ import { isNullish as e } from "../node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/isNullish.js";
2
+ import { omitBy as t } from "../node_modules/.pnpm/remeda@2.39.0/node_modules/remeda/dist/omitBy.js";
3
+ //#region src/url/index.ts
4
+ function n(e = "") {
5
+ let t = !/^(https?:)?\/\//.test(e);
6
+ try {
7
+ let n = new URL(e, t ? "http://n.n" : void 0);
8
+ return {
9
+ base: t ? n.pathname : `${n.origin}${n.pathname}`,
10
+ query: Object.fromEntries(n.searchParams),
11
+ hash: n.hash
12
+ };
13
+ } catch {
14
+ throw Error("Invalid URL.");
15
+ }
16
+ }
17
+ function r(n, r = !0) {
18
+ let { base: i = "", query: a = {}, hash: o = "" } = n, s = r ? t(a, e) : a, c = new URLSearchParams();
19
+ Object.entries(s).forEach(([e, t]) => {
20
+ c.append(e, String(t));
21
+ });
22
+ let l = o ? o.startsWith("#") ? o : `#${o}` : "", u = c.toString();
23
+ return u ? `${i}${i.includes("?") ? "&" : "?"}${u}${l}` : i + l;
24
+ }
25
+ //#endregion
26
+ export { n as urlParse, r as urlStringify };
27
+
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/url/index.ts"],"sourcesContent":["import { isNullish, omitBy } from 'remeda'\n\ntype QueryValue = string | number | boolean | undefined | null\ninterface URLObject {\n base: string\n query: Record<string, QueryValue>\n hash: string\n}\n\nexport function urlParse(url = ''): URLObject {\n const isRelative = !/^(https?:)?\\/\\//.test(url)\n\n try {\n // 如果 url 是相对路径,就借用 http://n.n 作为 base\n const parsed = new URL(url, isRelative ? 'http://n.n' : undefined)\n\n return {\n base: isRelative ? parsed.pathname : `${parsed.origin}${parsed.pathname}`,\n query: Object.fromEntries(parsed.searchParams),\n hash: parsed.hash\n }\n } catch {\n throw new Error('Invalid URL.')\n }\n}\n\nexport function urlStringify(opts: Partial<URLObject>, omitNil = true): string {\n const { base = '', query = {}, hash = '' } = opts\n\n const cleanQuery = omitNil ? omitBy(query, isNullish) : query\n const params = new URLSearchParams()\n\n Object.entries(cleanQuery).forEach(([key, val]) => {\n params.append(key, String(val))\n })\n\n // 修复 Hash 自动补全 # 的问题\n const normalizedHash = hash ? (hash.startsWith('#') ? hash : `#${hash}`) : ''\n const queryString = params.toString()\n if (!queryString) return base + normalizedHash\n\n const connector = base.includes('?') ? '&' : '?'\n return `${base}${connector}${queryString}${normalizedHash}`\n}\n"],"mappings":";;;AASA,SAAgB,EAAS,IAAM,IAAe;CAC5C,IAAM,IAAa,CAAC,kBAAkB,KAAK,CAAG;CAE9C,IAAI;EAEF,IAAM,IAAS,IAAI,IAAI,GAAK,IAAa,eAAe,KAAA,CAAS;EAEjE,OAAO;GACL,MAAM,IAAa,EAAO,WAAW,GAAG,EAAO,SAAS,EAAO;GAC/D,OAAO,OAAO,YAAY,EAAO,YAAY;GAC7C,MAAM,EAAO;EACf;CACF,QAAQ;EACN,MAAU,MAAM,cAAc;CAChC;AACF;AAEA,SAAgB,EAAa,GAA0B,IAAU,IAAc;CAC7E,IAAM,EAAE,UAAO,IAAI,WAAQ,CAAC,GAAG,UAAO,OAAO,GAEvC,IAAa,IAAU,EAAO,GAAO,CAAS,IAAI,GAClD,IAAS,IAAI,gBAAgB;CAEnC,OAAO,QAAQ,CAAU,CAAC,CAAC,SAAS,CAAC,GAAK,OAAS;EACjD,EAAO,OAAO,GAAK,OAAO,CAAG,CAAC;CAChC,CAAC;CAGD,IAAM,IAAiB,IAAQ,EAAK,WAAW,GAAG,IAAI,IAAO,IAAI,MAAU,IACrE,IAAc,EAAO,SAAS;CAIpC,OAHK,IAGE,GAAG,IADQ,EAAK,SAAS,GAAG,IAAI,MAAM,MAChB,IAAc,MAHlB,IAAO;AAIlC"}
@@ -0,0 +1,13 @@
1
+ export type ValueOf<T> = T[keyof T];
2
+ export type ArrayItem<T extends unknown[]> = T extends (infer R)[] ? R : never;
3
+ export type ArgumentType<T> = T extends (...args: (infer R)[]) => unknown ? R : never;
4
+ export type DeepPartial<T> = Partial<{
5
+ [K in keyof T]: DeepPartial<T[K]>;
6
+ }>;
7
+ export type DeepRequired<T> = Required<{
8
+ [K in keyof T]: DeepRequired<T[K]>;
9
+ }>;
10
+ export type ClassPropertyTypes<T> = {
11
+ [K in keyof T]: T[K];
12
+ };
13
+ export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@greypan/js-kit",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/pan-Z2l0aHVi/mono.git"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "main": "./dist/index.js",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./*": {
21
+ "types": "./dist/*/index.d.ts",
22
+ "import": "./dist/*/index.js"
23
+ },
24
+ "./go": {
25
+ "types": "./dist/paradigm/go.d.ts",
26
+ "import": "./dist/paradigm/go.js"
27
+ },
28
+ "./rust": {
29
+ "types": "./dist/paradigm/rust.d.ts",
30
+ "import": "./dist/paradigm/rust.js"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "dependencies": {},
38
+ "devDependencies": {
39
+ "vite": "npm:@voidzero-dev/vite-plus-core@latest",
40
+ "vite-plus": "latest"
41
+ },
42
+ "browserslist": [
43
+ "defaults"
44
+ ],
45
+ "scripts": {
46
+ "build": "run-p type-check \"build-only {@}\" --",
47
+ "build-only": "vp build",
48
+ "deploy": "npm publish",
49
+ "preview": "vp preview",
50
+ "test": "vp test run",
51
+ "type-check": "vue-tsc --build",
52
+ "dev": "vp build --watch"
53
+ }
54
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vite-plus/test'
2
+
3
+ import { asyncCompose, type Middleware } from '../compose'
4
+
5
+ describe('Compose 单元测试', () => {
6
+ describe('基本用法', () => {
7
+ it('应当按顺序运行中间件', async () => {
8
+ const calls: string[] = []
9
+
10
+ const middleware1: Middleware = async (ctx, next) => {
11
+ calls.push('m1-before')
12
+ await next()
13
+ calls.push('m1-after')
14
+ }
15
+ const middleware2: Middleware = async (ctx, next) => {
16
+ calls.push('m2-before')
17
+ await next()
18
+ calls.push('m2-after')
19
+ }
20
+ const middleware3: Middleware = async (ctx, next) => {
21
+ calls.push('m3')
22
+ await next()
23
+ }
24
+
25
+ const fn = asyncCompose(middleware1, middleware2, middleware3)
26
+
27
+ await fn({}, async () => {
28
+ calls.push('end')
29
+ })
30
+
31
+ expect(calls).toEqual(['m1-before', 'm2-before', 'm3', 'end', 'm2-after', 'm1-after'])
32
+ })
33
+ })
34
+
35
+ describe('compose 边界情况', () => {
36
+ it('空中间件数组时应直接调用外层 next', async () => {
37
+ const calls: string[] = []
38
+ const fn = asyncCompose()
39
+ await fn({}, async () => {
40
+ calls.push('end')
41
+ })
42
+ expect(calls).toEqual(['end'])
43
+ })
44
+
45
+ it('单个中间件时应正常执行并调用外层 next', async () => {
46
+ const calls: string[] = []
47
+ const fn = asyncCompose(async (ctx, next) => {
48
+ calls.push('single')
49
+ await next()
50
+ })
51
+ await fn({}, async () => {
52
+ calls.push('end')
53
+ })
54
+ expect(calls).toEqual(['single', 'end'])
55
+ })
56
+
57
+ it('多次调用同一个 compose 函数时索引不应污染', async () => {
58
+ const fn = asyncCompose(
59
+ async (ctx: { calls: string[] }, next) => {
60
+ ctx.calls.push('m1')
61
+ await next()
62
+ },
63
+ async (ctx: { calls: string[] }, next) => {
64
+ ctx.calls.push('m2')
65
+ await next()
66
+ }
67
+ )
68
+ const calls1: string[] = []
69
+ const calls2: string[] = []
70
+ await fn({ calls: calls1 }, async () => {
71
+ calls1.push('end')
72
+ })
73
+ await fn({ calls: calls2 }, async () => {
74
+ calls2.push('end')
75
+ })
76
+ expect(calls1).toEqual(['m1', 'm2', 'end'])
77
+ expect(calls2).toEqual(['m1', 'm2', 'end'])
78
+ })
79
+
80
+ it('中间件抛出错误时应正确传递异常', async () => {
81
+ const fn = asyncCompose(
82
+ async () => {
83
+ throw new Error('boom')
84
+ },
85
+ async (ctx, next) => {
86
+ await next()
87
+ }
88
+ )
89
+ await expect(fn({}, async () => {})).rejects.toThrow('boom')
90
+ })
91
+
92
+ it('同一个中间件里重复调用 next 应该抛错', async () => {
93
+ const fn = asyncCompose(
94
+ async (ctx, next) => {
95
+ await next()
96
+ await next() // 第二次调用,触发防护
97
+ },
98
+ async (ctx, next) => {
99
+ await next()
100
+ }
101
+ )
102
+ await expect(fn({}, async () => {})).rejects.toThrow('Should not call "await next()" multiple times!')
103
+ })
104
+ })
105
+ })
@@ -0,0 +1,23 @@
1
+ export type Next = () => Promise<void>
2
+ export type Middleware<T = unknown> = (ctx: T, next: Next) => Promise<void>
3
+
4
+ export function asyncCompose<T = unknown>(...fns: Middleware<T>[]): Middleware<T> {
5
+ return async function dispatch(ctx: T, next: Next, i = 0): Promise<void> {
6
+ if (!fns.length) return next()
7
+ const fn = fns[i]
8
+ if (!fn) return next()
9
+ return fn(
10
+ ctx,
11
+ // 为什么不是 () => dispatch(ctx, next, i + 1)?
12
+ // 利用闭包防止中间件重复调用 next
13
+ (() => {
14
+ let called = false
15
+ return async () => {
16
+ if (called) throw new Error('Should not call "await next()" multiple times!')
17
+ called = true
18
+ await dispatch(ctx, next, i + 1)
19
+ }
20
+ })()
21
+ )
22
+ }
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './number'
2
+ export * from './paradigm'
3
+ export * from './plugin'
4
+ export * from './random'
5
+ export * from './timer'
6
+ export * from './url'
7
+ export * from './utility-types'
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vite-plus/test'
2
+
3
+ import { clamp, toPrecision } from '..'
4
+
5
+ describe('number 单元测试', () => {
6
+ describe('toPrecision', () => {
7
+ it('应正确处理正数精度(四舍五入)', () => {
8
+ expect(toPrecision(1234.567, 2)).toBe(1234.57)
9
+ expect(toPrecision(1.45, 1)).toBe(1.5)
10
+ expect(toPrecision(1.44, 1)).toBe(1.4)
11
+ })
12
+
13
+ it('应正确处理负数精度(取整到十位/百位)', () => {
14
+ expect(toPrecision(1234.56, -1)).toBe(1230)
15
+ expect(toPrecision(1234.56, -2)).toBe(1200)
16
+ expect(toPrecision(1260.56, -2)).toBe(1300)
17
+ })
18
+
19
+ it('应正确处理精度为 0 的情况', () => {
20
+ expect(toPrecision(1234.56, 0)).toBe(1235)
21
+ })
22
+
23
+ it('应对非有限数字(NaN, Infinity)原样返回', () => {
24
+ expect(toPrecision(NaN, 2)).toBeNaN()
25
+ expect(toPrecision(Infinity, 2)).toBe(Infinity)
26
+ expect(toPrecision(-Infinity, 2)).toBe(-Infinity)
27
+ })
28
+ })
29
+
30
+ describe('clamp', () => {
31
+ it('数值在范围内时应返回原值', () => {
32
+ expect(clamp(5, 0, 10)).toBe(5)
33
+ expect(clamp(0, 0, 10)).toBe(0)
34
+ expect(clamp(10, 0, 10)).toBe(10)
35
+ })
36
+
37
+ it('数值小于最小值时应返回最小值', () => {
38
+ expect(clamp(-5, 0, 10)).toBe(0)
39
+ })
40
+
41
+ it('数值大于最大值时应返回最大值', () => {
42
+ expect(clamp(15, 0, 10)).toBe(10)
43
+ })
44
+
45
+ it('处理负数区间', () => {
46
+ expect(clamp(-20, -10, -5)).toBe(-10)
47
+ expect(clamp(-2, -10, -5)).toBe(-5)
48
+ })
49
+ })
50
+ })
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 转换为指定精度的数字
3
+ * @param val 需要转换的数字
4
+ * @param precision 精度。表示小数点后位数。如果传了负数(比如 -2),则取整到百位(1234.56->1200)。
5
+ * @returns 转换后的数字
6
+ */
7
+ export function toPrecision(val: number, precision: number): number {
8
+ if (Number.isNaN(val) || !Number.isFinite(val)) return val
9
+
10
+ const factor = Math.pow(10, precision)
11
+ return Math.round(val * factor) / factor
12
+ }
13
+
14
+ /**
15
+ * 将数字限制在指定的范围内
16
+ * @param val 当前值
17
+ * @param min 最小值
18
+ * @param max 最大值
19
+ */
20
+ export function clamp(val: number, min: number, max: number): number {
21
+ return Math.max(min, Math.min(val, max))
22
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vite-plus/test'
2
+
3
+ import { err, isErr, isOk, ok, to, unwrap } from '../go'
4
+
5
+ describe('Result 工具函数(Go 风格)单元测试', () => {
6
+ it('ok() 应当返回 Ok<T>', () => {
7
+ const result = ok(42)
8
+ expect(result).toEqual([null, 42])
9
+ expect(isOk(result)).toBe(true)
10
+ expect(isErr(result)).toBe(false)
11
+ })
12
+
13
+ it('err() 应当返回 Err<E>', () => {
14
+ const error = new Error('Failed.')
15
+ const result = err(error)
16
+ expect(result).toEqual([error, null])
17
+ expect(isErr(result)).toBe(true)
18
+ expect(isOk(result)).toBe(false)
19
+ })
20
+
21
+ it('isOk() 和 isErr() 应当正确区分结果', () => {
22
+ const success = ok(123)
23
+ const failure = err('error')
24
+
25
+ expect(isOk(success)).toBe(true)
26
+ expect(isErr(success)).toBe(false)
27
+
28
+ expect(isOk(failure)).toBe(false)
29
+ expect(isErr(failure)).toBe(true)
30
+ })
31
+
32
+ it('to() 应当把成功的 Promise 包装成 Ok', async () => {
33
+ const result = await to(Promise.resolve('hi'))
34
+ expect(isOk(result)).toBe(true)
35
+ expect(result).toEqual([null, 'hi'])
36
+ })
37
+
38
+ it('to() 应当把失败的 Promise 包装成 Err', async () => {
39
+ const result = await to(Promise.reject('error'))
40
+ expect(isErr(result)).toBe(true)
41
+ expect(result).toEqual(['error', null])
42
+ })
43
+
44
+ it('unwrap() 在 Ok 时返回值', () => {
45
+ const result = ok('data')
46
+ expect(unwrap(result)).toBe('data')
47
+ })
48
+
49
+ it('unwrap() 在 Err 时返回错误', () => {
50
+ const result = err('error')
51
+ expect(unwrap(result)).toBe('error')
52
+ })
53
+
54
+ it('Result 数据结构应当是 [error, value]', () => {
55
+ const success = ok(1)
56
+ expect(success[0]).toBeNull()
57
+ expect(success[1]).toBe(1)
58
+
59
+ const failure = err('boom')
60
+ expect(failure[0]).toBe('boom')
61
+ expect(failure[1]).toBeNull()
62
+ })
63
+ })
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from 'vite-plus/test'
2
+
3
+ import { err, isErr, isOk, ok, to, unwrap } from '../rust'
4
+
5
+ describe('Result 工具函数(Rust 风格)单元测试', () => {
6
+ it('ok() 应当返回一个 Ok<T>', () => {
7
+ const result = ok(123)
8
+ expect(result).toEqual({ ok: true, value: 123 })
9
+ expect(isOk(result)).toBe(true)
10
+ expect(isErr(result)).toBe(false)
11
+ })
12
+
13
+ it('err() 应当返回一个 Err<E>', () => {
14
+ const result = err('error')
15
+ expect(result).toEqual({ ok: false, error: 'error' })
16
+ expect(isErr(result)).toBe(true)
17
+ expect(isOk(result)).toBe(false)
18
+ })
19
+
20
+ it('isOk() 和 isErr() 应当正确区分结果', () => {
21
+ const success = ok('success')
22
+ const failure = err('fail')
23
+
24
+ expect(isOk(success)).toBe(true)
25
+ expect(isErr(success)).toBe(false)
26
+
27
+ expect(isOk(failure)).toBe(false)
28
+ expect(isErr(failure)).toBe(true)
29
+ })
30
+
31
+ it('to() 应当把成功的 Promise 包装成 Ok', async () => {
32
+ const result = await to(Promise.resolve('data'))
33
+ expect(result).toEqual({ ok: true, value: 'data' })
34
+ expect(isOk(result)).toBe(true)
35
+ })
36
+
37
+ it('to() 应当把失败的 Promise 包装成 Err', async () => {
38
+ const result = await to(Promise.reject('error'))
39
+ expect(result).toEqual({ ok: false, error: 'error' })
40
+ expect(isErr(result)).toBe(true)
41
+ })
42
+
43
+ it('unwrap() 在 Ok 时返回值', () => {
44
+ const result = ok('data')
45
+ expect(unwrap(result)).toBe('data')
46
+ })
47
+
48
+ it('unwrap() 在 Err 时返回错误', () => {
49
+ const result = err('error')
50
+ expect(unwrap(result)).toBe('error')
51
+ })
52
+
53
+ it('Ok<T> 应当只有 ok 和 value 属性,没有 error', () => {
54
+ const result = ok('success')
55
+ expect(result).toHaveProperty('ok')
56
+ expect(result.ok).toBe(true)
57
+ expect(result).toHaveProperty('value')
58
+ expect(result).not.toHaveProperty('error')
59
+ })
60
+
61
+ it('Err<E> 应当只有 ok 和 error 属性,没有 value', () => {
62
+ const result = err('fail')
63
+ expect(result).toHaveProperty('ok')
64
+ expect(result.ok).toBe(false)
65
+ expect(result).toHaveProperty('error')
66
+ expect(result).not.toHaveProperty('value')
67
+ })
68
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @description Go 范式,用于统一处理成功/失败返回值
3
+ */
4
+
5
+ export type Ok<T> = readonly [null, T]
6
+ export type Err<E> = readonly [E, null]
7
+ export type Result<T, E> = Ok<T> | Err<E>
8
+
9
+ export function ok<T>(value: T): Ok<T> {
10
+ return [null, value]
11
+ }
12
+
13
+ export function err<E>(error: E): Err<E> {
14
+ return [error, null]
15
+ }
16
+
17
+ export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
18
+ return result[0] === null && result[1] !== null
19
+ }
20
+
21
+ export function isErr<T, E>(result: Result<T, E>): result is Err<E> {
22
+ return result[0] !== null && result[1] === null
23
+ }
24
+
25
+ export async function to<T, E = unknown>(promise: Promise<T>): Promise<Result<Awaited<T>, E>> {
26
+ try {
27
+ return ok(await promise)
28
+ } catch (error) {
29
+ return err(error as E)
30
+ }
31
+ }
32
+
33
+ export function unwrap<S extends Result<any, any>>(
34
+ result: S
35
+ ): S extends Ok<infer T> ? T : S extends Err<infer E> ? E : never {
36
+ const [error, value] = result
37
+ return isOk(result) ? value : error
38
+ }
@@ -0,0 +1,2 @@
1
+ export * as go from './go'
2
+ export * as rust from './rust'