• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alovajs / alova / #219

01 Nov 2024 02:50PM UTC coverage: 95.359% (+1.5%) from 93.83%
#219

push

github

web-flow
Merge pull request #577 from alovajs/changeset-release/main

ci: release

1698 of 1787 branches covered (95.02%)

Branch coverage included in aggregate %.

5801 of 6077 relevant lines covered (95.46%)

223.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.87
/packages/client/src/util/helper.ts
1
import {
2✔
2
  $self,
3
  FrameworkReadableState,
4
  FrameworkState,
5
  GeneralFn,
6
  GeneralState,
7
  ObjectCls,
8
  clearTimeoutTimer,
9
  createAssert,
10
  falseValue,
11
  filterItem,
12
  forEach,
13
  includes,
14
  instanceOf,
15
  isFn,
16
  isNumber,
17
  mapItem,
18
  newInstance,
19
  noop,
20
  nullValue,
21
  objAssign,
22
  objectKeys,
23
  pushItem,
24
  setTimeoutFn,
25
  trueValue
26
} from '@alova/shared';
27
import { Method } from 'alova';
28

29
import type { AlovaGenerics, EffectRequestParams, ReferingObject, StatesExport, StatesHook } from 'alova';
30
import type { AlovaMethodHandler, ExportedComputed, ExportedState } from '~/typings/clienthook';
31

32
/**
33
 * Compatible functions, throwing parameters
34
 * @param error mistake
35
 */
36
export const throwFn = <T>(error: T) => {
2✔
37
  throw error;
4✔
38
};
4✔
39

40
export function useCallback<Fn extends GeneralFn = GeneralFn>(onCallbackChange: (callbacks: Fn[]) => void = noop) {
2✔
41
  let callbacks: Fn[] = [];
2✔
42

43
  const setCallback = (fn: Fn) => {
2✔
44
    if (!callbacks.includes(fn)) {
7✔
45
      callbacks.push(fn);
3✔
46
      onCallbackChange(callbacks);
3✔
47
    }
3✔
48
    // Return unregister function
49
    return () => {
7✔
50
      callbacks = filterItem(callbacks, e => e !== fn);
×
51
      onCallbackChange(callbacks);
×
52
    };
×
53
  };
7✔
54

55
  const triggerCallback = (...args: any[]) => {
2✔
56
    if (callbacks.length > 0) {
2✔
57
      return forEach(callbacks, fn => fn(...args));
2✔
58
    }
2✔
59
  };
2✔
60

61
  const removeAllCallback = () => {
2✔
62
    callbacks = [];
1✔
63
    onCallbackChange(callbacks);
1✔
64
  };
1✔
65

66
  return [setCallback, triggerCallback as Fn, removeAllCallback] as const;
2✔
67
}
2✔
68

69
/**
70
 * Create a debounce function and trigger the function immediately when delay is 0
71
 * Scenario: When calling useWatcher and setting immediate to true, the first call must be executed immediately, otherwise it will cause a delayed call
72
 * @param {GeneralFn} fn callback function
73
 * @param {number|(...args: any[]) => number} delay Delay description, dynamic delay can be achieved when set as a function
74
 * @returns Delayed callback function
75
 */
76
export const debounce = (fn: GeneralFn, delay: number | ((...args: any[]) => number)) => {
2✔
77
  let timer: any = nullValue;
476✔
78
  return function debounceFn(this: any, ...args: any[]) {
476✔
79
    const bindFn = fn.bind(this, ...args);
326✔
80
    const delayMill = isNumber(delay) ? delay : delay(...args);
326!
81
    timer && clearTimeoutTimer(timer);
326✔
82
    if (delayMill > 0) {
326✔
83
      timer = setTimeoutFn(bindFn, delayMill);
60✔
84
    } else {
326✔
85
      bindFn();
266✔
86
    }
266✔
87
  };
326✔
88
};
476✔
89

90
/**
91
 * Get the request method object
92
 * @param methodHandler Request method handle
93
 * @param args Method call parameters
94
 * @returns request method object
95
 */
96
export const getHandlerMethod = (methodHandler: Method | AlovaMethodHandler, args: any[] = []) => {
2✔
97
  const methodInstance = isFn(methodHandler) ? methodHandler(...args) : methodHandler;
86!
98
  createAssert('scene')(
86✔
99
    instanceOf(methodInstance, Method),
86✔
100
    'hook handler must be a method instance or a function that returns method instance'
86✔
101
  );
86✔
102
  return methodInstance;
86✔
103
};
86✔
104

105
/**
106
 * Convert each value of the object and return the new object
107
 * @param obj object
108
 * @param callback callback function
109
 * @returns converted object
110
 */
111
export const mapObject = <T extends Record<string, any>, U>(
2✔
112
  obj: T,
1,093✔
113
  callback: (value: T[keyof T], key: string, parent: T) => U
1,093✔
114
) => {
1,093✔
115
  const ret: Record<string, U> = {};
1,093✔
116
  for (const key in obj) {
1,093✔
117
    ret[key] = callback(obj[key], key, obj);
1,280✔
118
  }
1,280✔
119
  return ret as Record<keyof T, U>;
1,093✔
120
};
1,093✔
121

122
export const enum EnumHookType {
2✔
123
  USE_REQUEST = 1,
2✔
124
  USE_WATCHER = 2,
2✔
125
  USE_FETCHER = 3
2✔
126
}
127

128
interface MemorizedFunction {
129
  (...args: any[]): any;
130
  memorized: true;
131
}
132

133
type ActualStateTranslator<AG extends AlovaGenerics, StateProxy extends FrameworkReadableState<any, string>> =
134
  StateProxy extends FrameworkState<any, string>
135
    ? ExportedState<StateProxy['v'], AG['StatesExport']>
136
    : ExportedComputed<StateProxy['v'], AG['StatesExport']>;
137
type CompletedExposingProvider<AG extends AlovaGenerics, O extends Record<string | number | symbol, any>> = {
138
  [K in keyof O]: O[K] extends FrameworkReadableState<any, string>
139
    ? ActualStateTranslator<AG, O[K]>
140
    : // eslint-disable-next-line @typescript-eslint/no-unused-vars
141
      K extends `on${infer _}`
142
      ? (...args: Parameters<O[K]>) => CompletedExposingProvider<AG, O>
143
      : O[K];
144
};
145
/**
146
 * create simple and unified, framework-independent states creators and handlers.
147
 * @param statesHook states hook from `promiseStatesHook` function of alova
148
 * @param referingObject refering object exported from `promiseStatesHook` function
149
 * @returns simple and unified states creators and handlers
150
 */
151
export function statesHookHelper<AG extends AlovaGenerics>(
2✔
152
  statesHook: StatesHook<StatesExport<unknown>>,
1,637✔
153
  referingObject: ReferingObject = { trackedKeys: {}, bindError: falseValue }
1,637✔
154
) {
1,637✔
155
  const ref = <Data>(initialValue: Data) => (statesHook.ref ? statesHook.ref(initialValue) : { current: initialValue });
1,637✔
156
  referingObject = ref(referingObject).current;
1,637✔
157
  const exportState = <Data>(state: GeneralState<Data>) =>
1,637✔
158
    (statesHook.export || $self)(state, referingObject) as GeneralState<Data>;
8,063✔
159
  const memorize = <Callback extends GeneralFn>(fn: Callback) => {
1,637✔
160
    if (!isFn(statesHook.memorize)) {
8,235✔
161
      return fn;
1,900✔
162
    }
1,900✔
163
    const memorizedFn = statesHook.memorize(fn);
6,335✔
164
    (memorizedFn as unknown as MemorizedFunction).memorized = true;
6,335✔
165
    return memorizedFn;
6,335✔
166
  };
8,235✔
167
  const { dehydrate } = statesHook;
1,637✔
168

169
  // For performance reasons, only value is different, and the key is tracked can be updated.
170
  const update = (newValue: any, state: GeneralState, key: string) =>
1,637✔
171
    newValue !== dehydrate(state, key, referingObject) &&
3,091✔
172
    referingObject.trackedKeys[key] &&
1,877✔
173
    statesHook.update(newValue, state, key, referingObject);
1,632✔
174
  const mapDeps = (deps: (GeneralState | FrameworkReadableState<any, string>)[]) =>
1,637✔
175
    mapItem(deps, item => (instanceOf(item, FrameworkReadableState) ? item.e : item));
962✔
176
  const createdStateList = [] as string[];
1,637✔
177

178
  // key of deps on computed
179
  const depKeys: Record<string, true> = {};
1,637✔
180

181
  return {
1,637✔
182
    create: <Data, Key extends string>(initialValue: Data, key: Key) => {
1,637✔
183
      pushItem(createdStateList, key); // record the keys of created states.
6,860✔
184
      return newInstance(
6,860✔
185
        FrameworkState<Data, Key>,
6,860✔
186
        statesHook.create(initialValue, key, referingObject) as GeneralState<Data>,
6,860✔
187
        key,
6,860✔
188
        state => dehydrate(state, key, referingObject),
6,860✔
189
        exportState,
6,860✔
190
        (state, newValue) => update(newValue, state, key)
6,860✔
191
      );
6,860✔
192
    },
6,860✔
193
    computed: <Data, Key extends string>(
1,637✔
194
      getter: () => Data,
634✔
195
      depList: (GeneralState | FrameworkReadableState<any, string>)[],
634✔
196
      key: Key
634✔
197
    ) => {
634✔
198
      // Collect all dependencies in computed
199
      forEach(depList, dep => {
634✔
200
        if (dep.k) {
1,902✔
201
          depKeys[dep.k as string] = true;
1,902✔
202
        }
1,902✔
203
      });
634✔
204

205
      return newInstance(
634✔
206
        FrameworkReadableState<Data, Key>,
634✔
207
        statesHook.computed(getter, mapDeps(depList), key, referingObject) as GeneralState<Data>,
634✔
208
        key,
634✔
209
        state => dehydrate(state, key, referingObject),
634✔
210
        exportState
634✔
211
      );
634✔
212
    },
634✔
213
    effectRequest: (effectRequestParams: EffectRequestParams<any>) =>
1,637✔
214
      statesHook.effectRequest(effectRequestParams, referingObject),
1,090✔
215
    ref,
1,637✔
216
    watch: (source: (GeneralState | FrameworkReadableState<any, string>)[], callback: () => void) =>
1,637✔
217
      statesHook.watch(mapDeps(source), callback, referingObject),
328✔
218
    onMounted: (callback: () => void) => statesHook.onMounted(callback, referingObject),
1,637✔
219
    onUnmounted: (callback: () => void) => statesHook.onUnmounted(callback, referingObject),
1,637✔
220
    memorize,
1,637✔
221

222
    /**
223
     * refering object that sharing some value with this object.
224
     */
225
    __referingObj: referingObject,
1,637✔
226

227
    /**
228
     * expose provider for specified use hook.
229
     * @param object object that contains state proxy, framework state, operating function and event binder.
230
     * @returns provider component.
231
     */
232
    exposeProvider: <O extends Record<string | number | symbol, any>>(object: O) => {
1,637✔
233
      const provider: Record<string | number | symbol, any> = {};
1,573✔
234
      const originalStatesMap: Record<string, GeneralState> = {};
1,573✔
235
      for (const key in object) {
1,573✔
236
        const value = object[key];
21,732✔
237
        const isValueFunction = isFn(value);
21,732✔
238
        // if it's a memorized function, don't memorize it any more, add it to provider directly.
239
        // if it's start with `on`, that indicates it is an event binder, we should define a new function which return provider object.
240
        // if it's a common function, add it to provider with memorize mode.
241

242
        // Note that: in some situation, state is a function such as solid's signal, and state value is set to function in react,  the state will be detected as a function. so we should check whether the key is in `trackedKeys`
243
        if (isValueFunction && !referingObject.trackedKeys[key]) {
21,732✔
244
          provider[key] = key.startsWith('on')
11,638✔
245
            ? (...args: any[]) => {
5,969✔
246
                value(...args);
3,205✔
247
                // eslint-disable-next-line
248
                return completedProvider;
3,205✔
249
              }
3,205✔
250
            : (value as MemorizedFunction).memorized
5,669✔
251
              ? value
1,056✔
252
              : memorize(value);
4,613✔
253
        } else {
21,622✔
254
          const isFrameworkState = instanceOf(value, FrameworkReadableState);
10,094✔
255
          if (isFrameworkState) {
10,094✔
256
            originalStatesMap[key] = value.s;
7,494✔
257
          }
7,494✔
258
          // otherwise, it's a state proxy or framework state, add it to provider with getter mode.
259
          ObjectCls.defineProperty(provider, key, {
10,094✔
260
            get: () => {
10,094✔
261
              // record the key that is being tracked.
262
              referingObject.trackedKeys[key] = trueValue;
6,309✔
263
              return isFrameworkState ? value.e : value;
6,309✔
264
            },
6,309✔
265

266
            // set need to set an function,
267
            // otherwise it will throw `TypeError: Cannot set property __referingObj of #<Object> which has only a getter` when setting value
268
            set: noop,
10,094✔
269
            enumerable: trueValue,
10,094✔
270
            configurable: trueValue
10,094✔
271
          });
10,094✔
272
        }
10,094✔
273
      }
21,732✔
274

275
      const { update: nestedHookUpdate, __proxyState: nestedProxyState } = provider;
1,573✔
276
      // reset the tracked keys and bingError flag, so that the nest hook providers can be initialized.
277
      // Always track the dependencies in computed
278
      referingObject.trackedKeys = {
1,573✔
279
        ...depKeys
1,573✔
280
      };
1,573✔
281
      referingObject.bindError = falseValue;
1,573✔
282

283
      const extraProvider = {
1,573✔
284
        // expose referingObject automatically.
285
        __referingObj: referingObject,
1,573✔
286

287
        // the new updating function that can update the new states and nested hook states.
288
        update: memorize(
1,573✔
289
          (newStates: {
1,573✔
290
            [K in keyof O]?: any;
291
          }) => {
51✔
292
            objectKeys(newStates).forEach(key => {
51✔
293
              if (includes(createdStateList, key)) {
60✔
294
                update(newStates[key], originalStatesMap[key], key);
58✔
295
              } else if (key in provider && isFn(nestedHookUpdate)) {
60✔
296
                nestedHookUpdate({
2✔
297
                  [key]: newStates[key]
2✔
298
                });
2✔
299
              }
2✔
300
            });
51✔
301
          }
51✔
302
        ),
1,573✔
303
        __proxyState: memorize(<K extends keyof O>(key: K) => {
1,573✔
304
          if (includes(createdStateList, key as string) && instanceOf(object[key], FrameworkReadableState)) {
322✔
305
            // need to tag the key that is being tracked so that it can be updated with `state.v = xxx`.
306
            referingObject.trackedKeys[key as string] = trueValue;
322✔
307
            return object[key];
322✔
308
          }
322✔
309
          return nestedProxyState(key);
×
310
        })
1,573✔
311
      };
1,573✔
312

313
      const completedProvider = objAssign(provider, extraProvider) as CompletedExposingProvider<
1,573✔
314
        AG,
315
        O & typeof extraProvider
316
      >;
317
      return completedProvider;
1,573✔
318
    },
1,573✔
319

320
    /**
321
     * transform state proxies to object.
322
     * @param states proxy array of framework states
323
     * @param filterKey filter key of state proxy
324
     * @returns an object that contains the states of target form
325
     */
326
    objectify: <S extends FrameworkReadableState<any, string>[], Key extends 's' | 'v' | 'e' | undefined = undefined>(
1,637✔
327
      states: S,
2,895✔
328
      filterKey?: Key
2,895✔
329
    ) =>
330
      states.reduce(
2,895✔
331
        (result, item) => {
2,895✔
332
          (result as any)[item.k] = filterKey ? item[filterKey] : item;
14,231✔
333
          return result;
14,231✔
334
        },
14,231✔
335
        {} as {
2,895✔
336
          [K in S[number]['k']]: Key extends undefined
337
            ? Extract<S[number], { k: K }>
338
            : Extract<S[number], { k: K }>[NonNullable<Key>];
339
        }
340
      ),
2,895✔
341

342
    transformState2Proxy: <Key extends string>(state: GeneralState<any>, key: Key) =>
1,637✔
343
      newInstance(
1,280✔
344
        FrameworkState<any, Key>,
1,280✔
345
        state,
1,280✔
346
        key,
1,280✔
347
        state => dehydrate(state, key, referingObject),
1,280✔
348
        exportState,
1,280✔
349
        (state, newValue) => update(newValue, state, key)
1,280✔
350
      )
1,280✔
351
  };
1,637✔
352
}
1,637✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc