@arrai-innovations/reactive-helpers 7.1.0 → 8.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.
@@ -1,3 +1,3 @@
1
1
  {
2
- "recommendations": ["dbaeumer.vscode-eslint", "znck.vue"]
2
+ "recommendations": ["dbaeumer.vscode-eslint", "znck.vue", "galloween.jsdoc-highlight-code"]
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arrai-innovations/reactive-helpers",
3
- "version": "7.1.0",
3
+ "version": "8.0.0",
4
4
  "description": "VueJS 3 utility composition functions to help manipulate objects and lists.",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -13,9 +13,7 @@ export class CancellableResolvable {
13
13
  const cancelResolvable = new Resolvable();
14
14
  newResolvable.promise.cancel = jest
15
15
  .fn()
16
- .mockImplementationOnce(async () => {
17
- return cancelResolvable.promise;
18
- })
16
+ .mockImplementationOnce(async () => cancelResolvable.promise)
19
17
  .mockRejectedValue(new Error("cancel already called"));
20
18
  Object.assign(this, newResolvable);
21
19
  this.cancel = cancelResolvable;
@@ -24,9 +24,7 @@ describe("use/listSubscription.spec.js", function () {
24
24
  const listInstanceModule = await import("../../../use/listInstance");
25
25
  crudList = jest
26
26
  .fn()
27
- .mockImplementationOnce(() => {
28
- return crudListResolvable[0].promise;
29
- })
27
+ .mockImplementationOnce(() => crudListResolvable[0].promise)
30
28
  .mockImplementation(() => {
31
29
  const newResolvable = new CancellableResolvable();
32
30
  crudListResolvable.push(newResolvable);
@@ -34,12 +34,8 @@ describe("use/objectSubscription.js", function () {
34
34
  });
35
35
  globalUnsubscribe = jest.fn();
36
36
  globalSubscribe = jest.fn();
37
- globalSubscribe.mockImplementation(() => {
38
- return Promise.resolve(globalUnsubscribe);
39
- });
40
- globalUnsubscribe.mockImplementation(() => {
41
- return true;
42
- });
37
+ globalSubscribe.mockImplementation(() => Promise.resolve(globalUnsubscribe));
38
+ globalUnsubscribe.mockImplementation(() => true);
43
39
  const imported = await import("../../../use/objectSubscription");
44
40
  imported.setObjectSubscriptionCrud({
45
41
  subscribe: globalSubscribe,
@@ -41,9 +41,7 @@ describe("utils/assignReactiveObject", function () {
41
41
  const tes = effectScope();
42
42
  let computedSum;
43
43
  tes.run(() => {
44
- computedSum = computed(() => {
45
- return target.a + target.b + target.c;
46
- });
44
+ computedSum = computed(() => target.a + target.b + target.c);
47
45
  });
48
46
  try {
49
47
  expect(computedSum.value).toBe(6);
@@ -1,11 +1,8 @@
1
- import { useDebugMessage } from "../utils";
2
1
  import identity from "lodash-es/identity";
3
2
  import isEqual from "lodash-es/isEqual";
4
3
  import { effectScope, nextTick, onScopeDispose, reactive, readonly, watch } from "vue";
5
4
  import { deepUnref } from "vue-deepunref";
6
5
 
7
- const watchFnDebugMessage = useDebugMessage(["cancellableIntent", "watch"]);
8
-
9
6
  /*
10
7
  * Calls your awaitable function with the arguments you pass in, when the watch arguments change and are all truthy.
11
8
  * Watch arguments should be a reactive object.
@@ -47,7 +44,6 @@ export function useCancellableIntent({ awaitableWithCancel, watchArguments = {},
47
44
  }
48
45
 
49
46
  const watchFn = () => {
50
- watchFnDebugMessage("watchFn called");
51
47
  let newWatchValues = deepUnref(Object.values(watchArguments));
52
48
  if (isEqual(newWatchValues, previousWatchValues)) {
53
49
  return;
@@ -1,4 +1,4 @@
1
- import { keyDiff, loadingCombine, useDebugMessage } from "../utils";
1
+ import { keyDiff, loadingCombine } from "../utils";
2
2
  import { listInstanceStateKeys } from "./listInstance";
3
3
  import { listRelatedStateKeys } from "./listRelated";
4
4
  import { listSubscriptionStateKeys } from "./listSubscription";
@@ -6,9 +6,6 @@ import { useWatchesRunning } from "./watchesRunning";
6
6
  import isEmpty from "lodash-es/isEmpty";
7
7
  import { computed, effectScope, onScopeDispose, reactive, toRef, watch } from "vue";
8
8
 
9
- const computedDebugMessage = useDebugMessage(["listCalculated", "computed"]);
10
- const watchDebugMessage = useDebugMessage(["listCalculated", "watch"]);
11
-
12
9
  export const listCalculatedStateKeys = [
13
10
  "calculatedObjects",
14
11
  "calculatedObjectsParentStateObjectsWatchRunning",
@@ -38,7 +35,6 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
38
35
  const calculatedObjectsEffectScopes = {};
39
36
 
40
37
  function parentStateObjectsWatch() {
41
- watchDebugMessage("parentStateObjectsWatch called");
42
38
  const { addedKeys, removedKeys } = keyDiff(
43
39
  Object.keys(parentState.objects),
44
40
  Object.keys(state.calculatedObjects)
@@ -57,7 +53,6 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
57
53
  }
58
54
 
59
55
  function calculatedObjectsWatch() {
60
- watchDebugMessage("calculatedObjectsWatch called");
61
56
  const calculatedObjectsRulesIsEmpty = !state.calculatedObjectsRules || isEmpty(state.calculatedObjectsRules);
62
57
  for (const objectKey of Object.keys(state.calculatedObjects)) {
63
58
  if (!calculatedObjectsEffectScopes[objectKey]) {
@@ -86,9 +81,9 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
86
81
  }
87
82
  calculatedObjectsEffectScopes[objectKey].run(() => {
88
83
  for (const addedRuleKey of addedRuleKeys) {
89
- calculatedObjectsObject[addedRuleKey] = computed(() => {
90
- return state.calculatedObjectsRules?.[addedRuleKey]?.(originalObject);
91
- });
84
+ calculatedObjectsObject[addedRuleKey] = computed(() =>
85
+ state.calculatedObjectsRules?.[addedRuleKey]?.(originalObject)
86
+ );
92
87
  }
93
88
  });
94
89
  }
@@ -125,12 +120,9 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
125
120
 
126
121
  watchesRunning = useWatchesRunning({
127
122
  triggerRefs: [
128
- computed(() => {
129
- computedDebugMessage("listCalculated trigger");
130
- return state.calculatedObjectsRules && !isEmpty(state.calculatedObjectsRules)
131
- ? parentState.loading
132
- : false;
133
- }),
123
+ computed(() =>
124
+ state.calculatedObjectsRules && !isEmpty(state.calculatedObjectsRules) ? parentState.loading : false
125
+ ),
134
126
  ],
135
127
  watchSentinelRefs: [
136
128
  toRef(state, "calculatedObjectsParentStateObjectsWatchRunning"),
@@ -138,10 +130,7 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
138
130
  ],
139
131
  });
140
132
 
141
- state.running = computed(() => {
142
- computedDebugMessage("listCalculated running");
143
- return loadingCombine(watchesRunning.state.running, parentState.running);
144
- });
133
+ state.running = computed(() => loadingCombine(watchesRunning.state.running, parentState.running));
145
134
 
146
135
  onScopeDispose(() => {
147
136
  for (const objectKey of Object.keys(calculatedObjectsEffectScopes)) {
@@ -129,9 +129,7 @@ export function useListInstance({ props, functions = {} }) {
129
129
  return Promise.reject(new ListError("already loading."));
130
130
  }
131
131
  let returnPromiseResolve;
132
- const returnPromise = new Promise((resolve) => {
133
- returnPromiseResolve = resolve;
134
- });
132
+ const returnPromise = new Promise((resolve) => (returnPromiseResolve = resolve));
135
133
  state.loading = true;
136
134
  state.errored = false;
137
135
  state.error = null;
@@ -1,4 +1,4 @@
1
- import { keyDiff, loadingCombine, useDebugMessage } from "../utils";
1
+ import { keyDiff, loadingCombine } from "../utils";
2
2
  import { listInstanceStateKeys } from "./listInstance";
3
3
  import { listSubscriptionStateKeys } from "./listSubscription";
4
4
  import { useWatchesRunning } from "./watchesRunning";
@@ -8,9 +8,6 @@ import isEmpty from "lodash-es/isEmpty";
8
8
  import isUndefined from "lodash-es/isUndefined";
9
9
  import { computed, effectScope, onScopeDispose, reactive, toRef, unref, watch } from "vue";
10
10
 
11
- const computedDebugMessage = useDebugMessage(["listRelated", "computed"]);
12
- const watchDebugMessage = useDebugMessage(["listRelated", "watch"]);
13
-
14
11
  export const listRelatedStateKeys = [
15
12
  "relatedObjects",
16
13
  "relatedObjectsRules",
@@ -37,7 +34,6 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
37
34
  const relatedObjectsEffectScopes = {};
38
35
 
39
36
  function parentStateObjectsWatch() {
40
- watchDebugMessage("parentStateObjectsWatch called");
41
37
  const { addedKeys, removedKeys } = keyDiff(Object.keys(parentState.objects), Object.keys(state.relatedObjects));
42
38
  for (const removedKey of removedKeys) {
43
39
  delete state.relatedObjects[removedKey];
@@ -53,7 +49,6 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
53
49
  }
54
50
 
55
51
  function relatedObjectsWatch() {
56
- watchDebugMessage("relatedObjectsWatch called");
57
52
  const relatedObjectsRulesIsEmpty = !state.relatedObjectsRules || isEmpty(state.relatedObjectsRules);
58
53
  for (const objectKey of Object.keys(state.relatedObjects)) {
59
54
  const relatedObjectsObject = state.relatedObjects[objectKey];
@@ -125,12 +120,9 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
125
120
 
126
121
  watchesRunning = useWatchesRunning({
127
122
  triggerRefs: [
128
- computed(() => {
129
- computedDebugMessage("listRelated triggerRefs");
130
- return state.relatedObjectsRules && !isEmpty(state.relatedObjectsRules)
131
- ? parentState.loading
132
- : false;
133
- }),
123
+ computed(() =>
124
+ state.relatedObjectsRules && !isEmpty(state.relatedObjectsRules) ? parentState.loading : false
125
+ ),
134
126
  ],
135
127
  watchSentinelRefs: [
136
128
  toRef(state, "relatedObjectsParentStateObjectsWatchRunning"),
@@ -138,10 +130,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
138
130
  ],
139
131
  });
140
132
 
141
- state.running = computed(() => {
142
- computedDebugMessage("listRelated running");
143
- return loadingCombine(watchesRunning.state.running, parentState.running);
144
- });
133
+ state.running = computed(() => loadingCombine(watchesRunning.state.running, parentState.running));
145
134
 
146
135
  onScopeDispose(() => {
147
136
  for (const objectKey of Object.keys(relatedObjectsEffectScopes)) {
@@ -1,11 +1,8 @@
1
- import { keyDiff, loadingCombine, useDebugMessage } from "../utils";
1
+ import { keyDiff, loadingCombine } from "../utils";
2
2
  import { useWatchesRunning } from "./watchesRunning";
3
3
  import isEmpty from "lodash-es/isEmpty";
4
4
  import { computed, effectScope, onScopeDispose, reactive, toRef, watch } from "vue";
5
5
 
6
- const computedDebugMessage = useDebugMessage(["objectCalculated", "computed"]);
7
- const watchDebugMessage = useDebugMessage(["objectCalculated", "watch"]);
8
-
9
6
  export function useObjectCalculateds(instances, args) {
10
7
  for (const [key, value] of Object.entries(args)) {
11
8
  useObjectCalculated({
@@ -68,7 +65,6 @@ export function useObjectCalculated({
68
65
  }
69
66
 
70
67
  watch([() => state.calculatedObjectRules && Object.keys(state.calculatedObjectRules)], () => {
71
- watchDebugMessage("calculatedObjectRules watch called");
72
68
  let addedKeys = [],
73
69
  removedKeys = [],
74
70
  sameKeys = [];
@@ -106,12 +102,7 @@ export function useObjectCalculated({
106
102
  });
107
103
 
108
104
  watchesRunning = useWatchesRunning({
109
- triggerRefs: [
110
- computed(() => {
111
- computedDebugMessage("watchesRunningTriggerRefs computed");
112
- return !isEmpty(state.calculatedObjectRules) ? parentState.loading : false;
113
- }),
114
- ],
105
+ triggerRefs: [computed(() => (!isEmpty(state.calculatedObjectRules) ? parentState.loading : false))],
115
106
  watchSentinelRefs: [
116
107
  toRef(state, "parentStateObjectWatchRunning"),
117
108
  toRef(state, "calculatedObjectWatchRunning"),
@@ -119,10 +110,7 @@ export function useObjectCalculated({
119
110
  });
120
111
 
121
112
  state.calculatedRunning = toRef(watchesRunning.state, "running");
122
- state.running = computed(() => {
123
- computedDebugMessage("running computed");
124
- return loadingCombine(watchesRunning.state.running, parentState.relatedRunning);
125
- });
113
+ state.running = computed(() => loadingCombine(watchesRunning.state.running, parentState.relatedRunning));
126
114
 
127
115
  onScopeDispose(() => {
128
116
  for (const key in calculatedObjectEffectScopes) {
@@ -1,4 +1,4 @@
1
- import { keyDiff, useDebugMessage } from "../utils";
1
+ import { keyDiff } from "../utils";
2
2
  import { useWatchesRunning } from "./watchesRunning";
3
3
  import get from "lodash-es/get";
4
4
  import isArray from "lodash-es/isArray";
@@ -6,9 +6,6 @@ import isEmpty from "lodash-es/isEmpty";
6
6
  import isUndefined from "lodash-es/isUndefined";
7
7
  import { computed, effectScope, onScopeDispose, reactive, toRef, unref, watch } from "vue";
8
8
 
9
- const computedDebugMessage = useDebugMessage(["objectRelated", "computed"]);
10
- const watchDebugMessage = useDebugMessage(["objectRelated", "watch"]);
11
-
12
9
  export function useObjectRelateds(instances, args) {
13
10
  for (const [key, value] of Object.entries(args)) {
14
11
  useObjectRelated({
@@ -64,7 +61,6 @@ export function useObjectRelated({
64
61
  }
65
62
 
66
63
  watch([() => state.relatedObjectRules && Object.keys(state.relatedObjectRules)], () => {
67
- watchDebugMessage("relatedObjectRules changed");
68
64
  let addedRuleKeys = [],
69
65
  removedRuleKeys = [];
70
66
  if (!state.relatedObjectRules) {
@@ -87,7 +83,6 @@ export function useObjectRelated({
87
83
  relatedObjectEffectScopes[addedRuleKey] = effectScope();
88
84
  relatedObjectEffectScopes[addedRuleKey].run(() => {
89
85
  state.relatedObjectObjects[addedRuleKey] = computed(() => {
90
- computedDebugMessage("relatedObjectObjects computed");
91
86
  // deal with computed objects being passed.
92
87
  const ruleObjects = unref(state.relatedObjectRules?.[addedRuleKey]?.objects);
93
88
  const rulePkKey = state.relatedObjectRules?.[addedRuleKey]?.pkKey || addedRuleKey;
@@ -108,12 +103,7 @@ export function useObjectRelated({
108
103
  });
109
104
 
110
105
  watchesRunning = useWatchesRunning({
111
- triggerRefs: [
112
- computed(() => {
113
- watchDebugMessage("parentStateObjectWatchRunning");
114
- return !isEmpty(state.relatedObjectRules) ? parentState.loading : false;
115
- }),
116
- ],
106
+ triggerRefs: [computed(() => (!isEmpty(state.relatedObjectRules) ? parentState.loading : false))],
117
107
  watchSentinelRefs: [
118
108
  toRef(state, "parentStateObjectWatchRunning"),
119
109
  toRef(state, "relatedObjectWatchRunning"),
@@ -1,9 +1,6 @@
1
- import { loadingCombine, useDebugMessage } from "../utils";
1
+ import { loadingCombine } from "../utils";
2
2
  import { computed, effectScope, reactive, unref, watch } from "vue";
3
3
 
4
- const computedDebugMessage = useDebugMessage(["watchesRunning", "computed"]);
5
- const watchDebugMessage = useDebugMessage(["watchesRunning", "watch"]);
6
-
7
4
  export function useWatchesRunning({ triggerRefs, watchSentinelRefs }) {
8
5
  const state = reactive({});
9
6
 
@@ -13,7 +10,6 @@ export function useWatchesRunning({ triggerRefs, watchSentinelRefs }) {
13
10
  watch(
14
11
  triggerRefs,
15
12
  (values) => {
16
- watchDebugMessage("useWatchesRunning triggerRefs");
17
13
  if (values.every((value) => unref(value))) {
18
14
  watchSentinelRefs.forEach((ref) => {
19
15
  ref.value = true;
@@ -27,7 +23,6 @@ export function useWatchesRunning({ triggerRefs, watchSentinelRefs }) {
27
23
  );
28
24
  state.running = computed(() => {
29
25
  const values = watchSentinelRefs.map((ref) => ref.value);
30
- computedDebugMessage("useWatchesRunning running");
31
26
  return loadingCombine(values);
32
27
  });
33
28
  });
@@ -11,12 +11,39 @@ export class AssignReactiveObjectError extends Error {
11
11
  }
12
12
  }
13
13
 
14
+ /**
15
+ * @typedef {Ref|object|array} ValidTargetOrSource targets and sources must be refs, objects, or arrays
16
+ * and refs must ultimately resolve to objects or arrays
17
+ */
18
+
19
+ /**
20
+ * Validates that a value is an array or an object, and throws an error if it is not.
21
+ *
22
+ * @param {string} key The key being validated.
23
+ * @param {*} value The value being validated.
24
+ * @throws {AssignReactiveObjectError} If the value is not an array or an object.
25
+ */
14
26
  function isArrayOrObject(key, value) {
15
27
  if (!(isArray(value) || isObject(value))) {
16
28
  throw new AssignReactiveObjectError(`${key} must be an object or an array, not ${inspect(value)}`);
17
29
  }
18
30
  }
19
31
 
32
+ /**
33
+ * @typedef validateTargetAndSourceResult
34
+ * @property {ValidTargetOrSource} target The validated target value.
35
+ * @property {ValidTargetOrSource} source The validated source value.
36
+ */
37
+
38
+ /**
39
+ * Validates that the target and source values are arrays or objects, and returns them.
40
+ * If either value is a ref, it is dereferenced before validation.
41
+ *
42
+ * @param {ValidTargetOrSource} target The target value to validate.
43
+ * @param {ValidTargetOrSource} source The source value to validate.
44
+ * @returns {validateTargetAndSourceResult} An object containing the validated target and source values.
45
+ * @throws {AssignReactiveObjectError} If either value is not an array or an object.
46
+ */
20
47
  function validateTargetAndSource(target, source) {
21
48
  isArrayOrObject("target", target);
22
49
  isArrayOrObject("source", source);
@@ -33,42 +60,90 @@ function validateTargetAndSource(target, source) {
33
60
  return { target, source };
34
61
  }
35
62
 
36
- function reactiveReplaceKeys(target, source, keys, exclude) {
63
+ /**
64
+ * Replaces keys in a target object or array with reactive refs to the corresponding keys in a
65
+ * source object or array.
66
+ *
67
+ * @param {ValidTargetOrSource} target The object receiving values.
68
+ * @param {ValidTargetOrSource} source The object providing values.
69
+ * @param {array} keys The keys to replace.
70
+ * @param {array} {exclude} Keys to exclude from replacement.
71
+ * @returns {boolean} True if any keys were replaced, false otherwise.
72
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
73
+ */
74
+ function reactiveReplaceKeys(target, source, keys, exclude = []) {
37
75
  const targetIsReactive = isReactive(target);
38
76
  const sourceIsReactive = isReactive(source);
77
+ let didAnything = false;
39
78
  for (const key of keys) {
40
79
  if (!exclude.includes(key)) {
41
80
  if (targetIsReactive && sourceIsReactive) {
42
81
  target[key] = toRef(source, key);
82
+ didAnything = true;
43
83
  } else if (target[key] !== source[key]) {
44
84
  target[key] = source[key];
85
+ didAnything = true;
45
86
  }
46
87
  }
47
88
  }
89
+ return didAnything;
48
90
  }
49
91
 
92
+ /**
93
+ * Adds to a target the missing keys from a source. `addedKeys` can be precalculated to avoid recalculation.
94
+ *
95
+ * @param {ValidTargetOrSource} target The object receiving values.
96
+ * @param {ValidTargetOrSource} source The object providing values.
97
+ * @param {array} {exclude} Keys to exclude from the addition.
98
+ * @param {array} {addedKeys} Precaulcated array of keys to add, if available. Otherwise, the
99
+ * keys will be calculated.
100
+ * @returns {boolean} True if any keys were added, false otherwise.
101
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
102
+ */
50
103
  export function addReactiveObject(target, source, exclude = [], addedKeys = null) {
51
104
  if (!addedKeys) {
52
105
  if (target === source) {
53
- return;
106
+ return false;
54
107
  }
55
108
  ({ target, source } = validateTargetAndSource(target, source));
56
109
  ({ addedKeys } = keyDiff(Object.keys(source) || [], Object.keys(target) || []));
57
110
  }
58
- reactiveReplaceKeys(target, source, addedKeys, exclude);
111
+ return reactiveReplaceKeys(target, source, addedKeys, exclude);
59
112
  }
60
113
 
114
+ /**
115
+ * Updates a target with mutually shared keys from a source. `sameKeys` can be precalculated to avoid recalculation.
116
+ *
117
+ * @param {ValidTargetOrSource} target The object receiving values.
118
+ * @param {ValidTargetOrSource} source The object providing values.
119
+ * @param {array} {exclude} Keys to exclude from the update.
120
+ * @param {array} {sameKeys} Precaulcated array of keys to update, if available. Otherwise, the
121
+ * keys will be calculated.
122
+ * @returns {boolean} True if any keys were updated, false otherwise.
123
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
124
+ */
61
125
  export function updateReactiveObject(target, source, exclude = [], sameKeys = null) {
62
126
  if (!sameKeys) {
63
127
  if (target === source) {
64
- return;
128
+ return false;
65
129
  }
66
130
  ({ target, source } = validateTargetAndSource(target, source));
67
131
  ({ sameKeys } = keyDiff(Object.keys(source) || [], Object.keys(target) || []));
68
132
  }
69
- reactiveReplaceKeys(target, source, sameKeys, exclude);
133
+ return reactiveReplaceKeys(target, source, sameKeys, exclude);
70
134
  }
71
135
 
136
+ /**
137
+ * Adds to a target the missing keys from a source, and updates a target with mutually shared keys from a source.
138
+ *
139
+ * @param {ValidTargetOrSource} target The object receiving values.
140
+ * @param {ValidTargetOrSource} source The object providing values.
141
+ * @param {array} {exclude} Keys to exclude from the addition or update.
142
+ * @param {array} {addedKeys} Precaulcated array of keys to add, if available. Otherwise, the
143
+ * keys will be calculated.
144
+ * @param {array} {sameKeys} Precaulcated array of keys to update, if available. Otherwise, the
145
+ * keys will be calculated.
146
+ */
72
147
  export function addOrUpdateReactiveObject(target, source, exclude = [], addedKeys = null, sameKeys = null) {
73
148
  if (!addedKeys && !sameKeys) {
74
149
  if (target === source) {
@@ -77,48 +152,91 @@ export function addOrUpdateReactiveObject(target, source, exclude = [], addedKey
77
152
  ({ target, source } = validateTargetAndSource(target, source));
78
153
  ({ addedKeys, sameKeys } = keyDiff(Object.keys(source) || [], Object.keys(target) || []));
79
154
  }
80
- addReactiveObject(target, source, exclude, addedKeys);
81
- updateReactiveObject(target, source, exclude, sameKeys);
155
+ let didAnything = false;
156
+ didAnything |= addReactiveObject(target, source, exclude, addedKeys);
157
+ didAnything |= updateReactiveObject(target, source, exclude, sameKeys);
158
+ return didAnything;
82
159
  }
83
160
 
84
- // there isn't much reactive about this I guess...
161
+ /**
162
+ * Removes keys from a target that are not present in a source.
163
+ *
164
+ * @param {ValidTargetOrSource} target The object receiving trimming.
165
+ * @param {ValidTargetOrSource|null} source The object that provides the allowed set of keys for calculating `removedKeys`.
166
+ * @param {array} {exclude} Keys to exclude from removal.
167
+ * @param {array} {removedKeys} An array to store removed keys.
168
+ * @returns {boolean} True if any keys were removed, false otherwise.
169
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
170
+ */
85
171
  export function trimReactiveObject(target, source, exclude = [], removedKeys = null) {
86
172
  if (!removedKeys) {
87
173
  if (target === source) {
88
- return;
174
+ return false;
89
175
  }
90
176
  ({ target, source } = validateTargetAndSource(target, source));
91
177
  ({ removedKeys } = keyDiff(Object.keys(source) || [], Object.keys(target) || []));
92
178
  }
93
179
  const targetIsArray = isArray(target);
180
+ let didAnything = false;
94
181
  if (targetIsArray) {
95
182
  // Remove indices in reverse (descending) order to keep them stable
96
183
  for (const removedKey of [...removedKeys].map((key) => parseInt(key, 10)).sort((a, b) => b - a)) {
97
184
  if (!exclude.includes(removedKey)) {
98
185
  target.splice(removedKey, 1);
186
+ didAnything = true;
99
187
  }
100
188
  }
101
189
  } else {
102
190
  for (const removedKey of removedKeys) {
103
191
  if (!exclude.includes(removedKey)) {
104
192
  delete target[removedKey];
193
+ didAnything = true;
105
194
  }
106
195
  }
107
196
  }
197
+ return didAnything;
108
198
  }
109
199
 
200
+ /**
201
+ * Change a target to match a source, where keys missing from the source are removed from the target,
202
+ * keys present in the source are added to the target, and keys present in both are updated in the target.
203
+ *
204
+ * @param {ValidTargetOrSource} target The target object or array.
205
+ * @param {ValidTargetOrSource} source The reactive object to assign.
206
+ * @param {array} {exclude} Keys to exclude from the assignment.
207
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
208
+ */
110
209
  export function assignReactiveObject(target, source, exclude = []) {
111
210
  if (target === source) {
112
211
  return;
113
212
  }
114
213
  ({ target, source } = validateTargetAndSource(target, source));
115
214
  const { addedKeys, sameKeys, removedKeys } = keyDiff(Object.keys(source) || [], Object.keys(target) || []);
116
- trimReactiveObject(target, removedKeys, exclude);
117
- addOrUpdateReactiveObject(target, source, exclude, addedKeys, sameKeys);
215
+ let didAnything = false;
216
+ didAnything |= trimReactiveObject(target, removedKeys, exclude);
217
+ didAnything |= addOrUpdateReactiveObject(target, source, exclude, addedKeys, sameKeys);
218
+ return didAnything;
118
219
  }
119
220
 
221
+ /**
222
+ * Recursively change a target to match a source, where keys missing from the source are removed from the target,
223
+ * keys present in the source are added to the target, and keys present in both are updated in the target.
224
+ *
225
+ * As an internal function, this function does not validate its arguments and has no optional arguments.
226
+ *
227
+ * @param {ValidTargetOrSource} target The object receiving updates.
228
+ * @param {ValidTargetOrSource} source The object providing updates.
229
+ * @param {array} exclude Keys to exclude from the update.
230
+ * @param {array} addedKeys Precaulcated array of keys to add, if available. Otherwise, the
231
+ * keys will be calculated.
232
+ * @param {array} sameKeys Precaulcated array of keys to update, if available. Otherwise, the
233
+ * keys will be calculated.
234
+ * @param {string} path The current path, used to rescope exclude for the next level.
235
+ * @param {function} fn The recursive function to call, likely the calling function itself.
236
+ */
120
237
  function recursiveInner(target, source, exclude, addedKeys, sameKeys, path, fn) {
121
- addReactiveObject(target, source, exclude, addedKeys);
238
+ let didAnything = false;
239
+ didAnything |= addReactiveObject(target, source, exclude, addedKeys);
122
240
  const keysForRecurse = [];
123
241
  const keysForReplace = [];
124
242
  for (const key of sameKeys) {
@@ -130,7 +248,7 @@ function recursiveInner(target, source, exclude, addedKeys, sameKeys, path, fn)
130
248
  }
131
249
  }
132
250
  }
133
- reactiveReplaceKeys(target, source, keysForReplace, exclude);
251
+ didAnything |= reactiveReplaceKeys(target, source, keysForReplace, exclude);
134
252
  for (const key of keysForRecurse) {
135
253
  // scope exclude for this next level, remove keys that don't start with the current path, trim keys that do to remove the current path
136
254
  const nextLevelExclude = exclude
@@ -139,29 +257,76 @@ function recursiveInner(target, source, exclude, addedKeys, sameKeys, path, fn)
139
257
  const nextPath = isArray(source[key]) ? `${path}[${key}]` : `${path}.${key}`;
140
258
  fn(target[key], source[key], nextLevelExclude, nextPath);
141
259
  }
260
+ return didAnything;
142
261
  }
143
262
 
263
+ /**
264
+ * Recursively change a target to match a source, where keys missing from the source are removed from the target,
265
+ * keys present in the source are added to the target, and keys present in both are updated in the target.
266
+ *
267
+ * An internal function to avoid validating arguments repeatedly.
268
+ *
269
+ * @param {ValidTargetOrSource} target The object receiving updates.
270
+ * @param {ValidTargetOrSource} source The object providing updates.
271
+ * @param {array} {exclude} Keys to exclude from the assignment.
272
+ * @param {string} {path} The current path, used to rescope exclude for the next level.
273
+ * @returns {boolean} True if any keys were added, updated, or removed, false otherwise.
274
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
275
+ */
144
276
  function assignReactiveObjectRecursive(target, source, exclude = [], path = "") {
145
277
  let { addedKeys, sameKeys, removedKeys } = keyDiff(Object.keys(source) || [], Object.keys(target) || []);
146
- trimReactiveObject(target, removedKeys, exclude);
147
- recursiveInner(target, source, exclude, addedKeys, sameKeys, path, assignReactiveObjectRecursive);
278
+ let didAnything = false;
279
+ didAnything |= trimReactiveObject(target, removedKeys, exclude);
280
+ didAnything |= recursiveInner(target, source, exclude, addedKeys, sameKeys, path, assignReactiveObjectRecursive);
281
+ return didAnything;
148
282
  }
149
283
 
284
+ /**
285
+ * Recursively change a target to match a source, where keys missing from the source are removed from the target,
286
+ * keys present in the source are added to the target, and keys present in both are updated in the target.
287
+ *
288
+ * @param {ValidTargetOrSource} target The object receiving updates.
289
+ * @param {ValidTargetOrSource} source The object providing updates.
290
+ * @param {array} {exclude} Keys to exclude from the assignment.
291
+ * @returns {boolean} True if any keys were added, updated, or removed, false otherwise.
292
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
293
+ */
150
294
  export function assignReactiveObjectDeep(target, source, exclude = []) {
151
295
  // exclude keys will need to be lodash get strings
152
296
  if (target === source) {
153
297
  return;
154
298
  }
155
299
  ({ target, source } = validateTargetAndSource(target, source));
156
- assignReactiveObjectRecursive(target, source, exclude);
300
+ return assignReactiveObjectRecursive(target, source, exclude);
157
301
  }
158
302
 
303
+ /**
304
+ * Recursively change a target to match a source, where keys present in the source are added to the target, and
305
+ * keys present in both are updated in the target. Missing keys are not removed.
306
+ *
307
+ * As an internal function, this function does not validate its argument.
308
+ *
309
+ * @param {ValidTargetOrSource} target The object receiving updates.
310
+ * @param {ValidTargetOrSource} source The object providing updates.
311
+ * @param {array} [exclude] Keys to exclude from the update.
312
+ * @param {string} [path] The current path, used to rescope exclude for the next level.
313
+ */
159
314
  function addOrUpdateReactiveObjectRecursive(target, source, exclude = [], path = "") {
160
315
  let addedKeys,
161
316
  sameKeys = keyDiff(Object.keys(source) || [], Object.keys(target) || []);
162
- recursiveInner(target, source, exclude, addedKeys, sameKeys, path, addOrUpdateReactiveObjectRecursive);
317
+ return recursiveInner(target, source, exclude, addedKeys, sameKeys, path, addOrUpdateReactiveObjectRecursive);
163
318
  }
164
319
 
320
+ /**
321
+ * Recursively change a target to match a source, where keys present in the source are added to the target, and
322
+ * keys present in both are updated in the target. Missing keys are not removed.
323
+ *
324
+ * @param {ValidTargetOrSource} target The object receiving updates.
325
+ * @param {ValidTargetOrSource} source The object providing updates.
326
+ * @param {array} [exclude] Keys to exclude from the update.
327
+ * @returns {boolean} True if any keys were added or updated, false otherwise.
328
+ * @throws {AssignReactiveObjectError} If either target or source are not ultimately objects or arrays.
329
+ */
165
330
  export function addOrUpdateReactiveObjectDeep(target, source, exclude = []) {
166
331
  // exclude keys will need to be lodash get strings
167
332
  if (target === source) {
@@ -1,54 +1,197 @@
1
- import inspect from "browser-util-inspect";
1
+ import { isSuperset } from "./set";
2
+ import { transformWalk } from "./transformWalk";
3
+ import { isSet, partial, union } from "lodash-es";
4
+ import debounce from "lodash-es/debounce";
2
5
  import { unref } from "vue";
3
6
 
7
+ /**
8
+ * Whether debug messages are enabled or not. For deploying to production with debugging but not
9
+ * spamming everyone with debug messages.
10
+ *
11
+ * @type {boolean}
12
+ */
4
13
  window.RH_DEBUG = false;
5
- window.RH_DEBUG_ENABLED_CATEGORIES = {};
6
- window.RH_DEBUG_DISABLED_CATEGORIES = {};
7
14
 
15
+ /**
16
+ * Map of categories to whether they are enabled or not. The order of the entries are important.
17
+ * All categories in the key must be present for the entry to match.
18
+ * The first matching entry with a value opf true will show the message.
19
+ * The first matching entry with a value of false will hide the message.
20
+ * The special category "*" will match all messages.
21
+ * Strings passed as keys will be treated as a single category.
22
+ * Sets can also be passed as keys, which is what the arrays get converted to.
23
+ *
24
+ * @type {Map<string[]|string|Set, boolean>}
25
+ * @example
26
+ * window.RH_DEBUG = true; // turn it on
27
+ * window.RH_DEBUG_CATEGORIES.set(["ofc-form-component", "lifecycle"], true); // show messages matching both categories
28
+ * window.RH_DEBUG_CATEGORIES.set(["lifecycle"], false); // turn off lifecycle messages not matching the above
29
+ * window.RH_DEBUG_CATEGORIES.set(["dropDown"], true); // show dropDown messages except where disabled by the above
30
+ * window.RH_DEBUG_CATEGORIES.set(["*"], false); // turn off all messages not matching the above
31
+ */
32
+ window.RH_DEBUG_CATEGORIES = new Map();
33
+
34
+ /**
35
+ * Group identical messages together and show a count of how many times they were logged.
36
+ * Messages are only shown on the trailing edge, so logging is delayed and somewhat out of order.
37
+ *
38
+ * @type {boolean}
39
+ */
40
+ window.RH_DEBOUNCE_DEBUG = false;
41
+
42
+ /**
43
+ * Whether to process arguments to be more friendly for console.log, with regard to circular references
44
+ * and not recursing into vue components.
45
+ *
46
+ * @type {boolean}
47
+ */
48
+ window.RH_TRANSFORM_MESSAGES = true;
49
+
50
+ const DEBOUNCE_WAIT = 50;
8
51
  const log = console.log;
9
- const messageHolder = [];
52
+ const messageBounceFns = {};
53
+ const counts = {};
10
54
 
11
55
  /**
12
- * @param {String} categoriesString
13
- * @param {(string|Function)[]} messages
56
+ * @param {Set} categoriesSet categories for the message log
57
+ * @param {string} categoriesKey key for debouncing
58
+ * @param {*[]} messages
14
59
  */
15
- const doLog = (categoriesString, messages) => {
60
+ const doLog = (categoriesSet, categoriesKey, messages) => {
16
61
  if (messages.length > 0) {
17
- for (const message of messages) {
18
- messageHolder.push(inspect(unref(typeof message === "function" ? message() : message)));
62
+ const key = getKey(categoriesKey, messages);
63
+ const count = counts[key];
64
+ const logArgs = [Array.from(categoriesSet).join(","), ...messages];
65
+ if (count > 1) {
66
+ logArgs.push(`(${count})`);
19
67
  }
20
- log(categoriesString, ...messageHolder);
21
- messageHolder.length = 0;
68
+ log(...logArgs);
69
+ delete counts[key];
70
+ delete messageBounceFns[key];
22
71
  }
23
72
  };
24
73
 
25
74
  /**
75
+ * Process a value for logging, dealing with circular references and
76
+ * not recursing into vue components.
26
77
  *
27
- * @param {string[]} categories
28
- * @returns {function(...((string|Function)[]|string|Function)): void}
78
+ * @param {Map} seenObjects for circlular reference detection
79
+ * @param {string} key keys is an unused argument from walk
80
+ * @param {*} value value to process
81
+ * @param {string} path path to the value, used for display in circular references
82
+ * @returns {*} processed value
83
+ */
84
+ export const inspectWalkFn = (seenObjects, key, value, path) => {
85
+ // return primatives as-is
86
+ if (typeof value !== "object" || value === null) {
87
+ return value;
88
+ }
89
+ if (seenObjects.has(value)) {
90
+ return `「Dupe:${seenObjects.get(value)}」`;
91
+ }
92
+ seenObjects.set(value, path);
93
+ if (value?.type?.__name) {
94
+ // vue component instance
95
+ return `⧼ Component:${value.type.__name} ⧽`;
96
+ }
97
+ return value;
98
+ };
99
+
100
+ /**
101
+ * @param {(string|function)[]} messages messages to resolve
102
+ * @returns {*[]} resolved messages
103
+ */
104
+ const resolveMessages = (messages) => {
105
+ const resolvedMessages = [];
106
+ for (const message of messages) {
107
+ let toPush = unref(typeof message === "function" ? message() : message);
108
+ if (window.RH_TRANSFORM_MESSAGES) {
109
+ const seenObjects = new Map();
110
+ toPush = transformWalk(toPush, partial(inspectWalkFn, seenObjects));
111
+ }
112
+ resolvedMessages.push(toPush);
113
+ }
114
+ return resolvedMessages;
115
+ };
116
+
117
+ /**
118
+ * @param {Set} categoriesSet categories for the message log
119
+ * @param {string} categoriesKey key for debouncing
120
+ * @param {*[]} messages messages to log
121
+ */
122
+ const doDebouncedLog = (categoriesSet, categoriesKey, messages) => {
123
+ if (!window.RH_DEBOUNCE_DEBUG) {
124
+ return doLog(categoriesSet, categoriesKey, messages);
125
+ }
126
+ const key = getKey(categoriesKey, messages);
127
+ let debouncedLog = messageBounceFns[key];
128
+ if (!debouncedLog) {
129
+ debouncedLog = debounce(doLog, DEBOUNCE_WAIT, { leading: false, trailing: true });
130
+ messageBounceFns[key] = debouncedLog;
131
+ }
132
+ debouncedLog(categoriesSet, categoriesKey, messages);
133
+ counts[key] = (counts[key] || 0) + 1;
134
+ };
135
+
136
+ /**
137
+ * @param {string} categoriesKey categories for the message log
138
+ * @param {(string|function)[]} messages messages to log
139
+ * @returns {string} key
140
+ **/
141
+ const getKey = (categoriesKey, messages) => `${categoriesKey}|${messages.join("-")}`;
142
+
143
+ /**
144
+ * @typedef {Object} DebugMessageFunction
145
+ * @property {function(...((string|function)[]|string|function)): void} log log a message
146
+ */
147
+
148
+ /**
149
+ * @param {string[]} categories categories for the message log
150
+ * @returns {DebugMessageFunction} debug message function
29
151
  */
30
152
  export function useDebugMessage(categories) {
31
- const categoriesSet = new Set(categories);
32
- const categoriesString = categoriesSet.size > 0 ? `[${Array.from(categoriesSet).join(", ")}]` : "";
33
- return (...messages) => {
153
+ const categoriesSet = isSet(categories) ? categories : new Set(categories);
154
+ const sortedCategories = Array.from(categoriesSet).sort();
155
+ const categoriesKey = categoriesSet.size > 0 ? `[${sortedCategories.join(", ")}]` : "";
156
+
157
+ const debugMessage = (...messages) => {
34
158
  if (!window.RH_DEBUG) {
35
159
  return;
36
160
  }
37
- if (categoriesSet.size > 0) {
38
- for (const category of categoriesSet) {
39
- if (window.RH_DEBUG_DISABLED_CATEGORIES[category]) {
40
- return;
41
- }
161
+ for (const [rule, enabledOrDisabled] of window.RH_DEBUG_CATEGORIES.entries()) {
162
+ let ruleSet;
163
+ if (Array.isArray(rule)) {
164
+ ruleSet = new Set(rule);
165
+ } else if (typeof rule === "string") {
166
+ ruleSet = new Set(rule.split(","));
167
+ } else if (rule.toString() === "[object Set]") {
168
+ ruleSet = rule;
169
+ } else if (rule === "*") {
170
+ ruleSet = new Set(["*"]);
171
+ } else {
172
+ throw new Error(`Unexpected rule type: ${typeof rule}, ${rule}`);
42
173
  }
43
- for (const category of categoriesSet) {
44
- if (window.RH_DEBUG_ENABLED_CATEGORIES[category]) {
45
- doLog(categoriesString, messages);
174
+ // if categoriesSet is a superset of ruleSet, then apply the rule
175
+ // first applicable rule wins
176
+ if (isSuperset(categoriesSet, ruleSet) || ruleSet.has("*")) {
177
+ if (enabledOrDisabled === true) {
178
+ doDebouncedLog(categoriesSet, categoriesKey, resolveMessages(messages));
179
+ return;
180
+ } else if (enabledOrDisabled === false) {
46
181
  return;
47
182
  }
183
+ // ignore other values
48
184
  }
49
185
  }
50
- if (categoriesSet.size === 0 || Object.keys(window.RH_DEBUG_ENABLED_CATEGORIES).length === 0) {
51
- doLog(categoriesString, messages);
52
- }
186
+ doDebouncedLog(categoriesSet, categoriesKey, resolveMessages(messages));
187
+ };
188
+ /**
189
+ * @param {string[]} moreCategories categories to add
190
+ * @returns {DebugMessageFunction} new debug message function
191
+ */
192
+ debugMessage.more = (moreCategories) => {
193
+ // return a new debugMessage so the added categories are only applied to the new debugMessage
194
+ return useDebugMessage(union(categoriesSet, isSet(moreCategories) ? moreCategories : new Set(moreCategories)));
53
195
  };
196
+ return debugMessage;
54
197
  }
@@ -1,6 +1,15 @@
1
1
  import isArray from "lodash-es/isArray";
2
2
  import isObject from "lodash-es/isObject";
3
3
 
4
+ /**
5
+ * Turn an array or object into an array of path strings. Recurses for any found arrays or objects.
6
+ *
7
+ * Array indexes are wrapped in square brackets and object keys are prefixed with a period.
8
+ *
9
+ * @param {Array|Object} arrayOrObject array or object to flatten
10
+ * @param {string} currentPath current path, for recursion or as a starting point
11
+ * @returns {string[]} paths
12
+ */
4
13
  export function flattenPaths(arrayOrObject, currentPath = "") {
5
14
  // arrayOrObject keys or indexes values can be objects or arrays.
6
15
  // find all paths you could use lodash to "get()" to.
@@ -10,15 +19,14 @@ export function flattenPaths(arrayOrObject, currentPath = "") {
10
19
  const keysOrIndexes = isArray(arrayOrObject);
11
20
  const dotOrNot = currentPath ? "." : "";
12
21
  if (isObject(arrayOrObject)) {
13
- Object.keys(arrayOrObject).forEach((key) => {
14
- const value = arrayOrObject[key];
22
+ for (const [key, value] of Object.entries(arrayOrObject)) {
15
23
  const keyPath = keysOrIndexes ? `[${key}]` : `${dotOrNot}${key}`;
16
24
  if (isObject(value) || isArray(value)) {
17
25
  paths.push(...flattenPaths(value, `${currentPath}${keyPath}`));
18
26
  } else {
19
27
  paths.push(`${currentPath}${keyPath}`);
20
28
  }
21
- });
29
+ }
22
30
  } else {
23
31
  // values
24
32
  if (currentPath) {
package/utils/keyDiff.js CHANGED
@@ -1,5 +1,29 @@
1
+ import { flattenPaths } from "./flattenPaths.js";
1
2
  import { difference, intersection } from "./set.js";
2
3
 
4
+ /**
5
+ * @typedef {object} KeyDiffOptions
6
+ * @property {boolean} [sameKeys=true]
7
+ * @property {boolean} [removedKeys=true]
8
+ * @property {boolean} [addedKeys=true]
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} KeyDiffResult
13
+ * @property {string[]} [sameKeys]
14
+ * @property {string[]} [removedKeys]
15
+ * @property {string[]} [addedKeys]
16
+ */
17
+
18
+ /**
19
+ * Calculate the difference between two arrays of keys, in terms of what keys
20
+ * are the same, what keys are removed, and what keys are added.
21
+ *
22
+ * @param {string[]} newKeys
23
+ * @param {string[]} oldKeys
24
+ * @param {KeyDiffOptions} [options]
25
+ * @returns {KeyDiffResult}
26
+ */
3
27
  export function keyDiff(newKeys, oldKeys, { sameKeys = true, removedKeys = true, addedKeys = true } = {}) {
4
28
  const newKeysSet = new Set(newKeys);
5
29
  const oldKeysSet = new Set(oldKeys);
@@ -15,3 +39,19 @@ export function keyDiff(newKeys, oldKeys, { sameKeys = true, removedKeys = true,
15
39
  }
16
40
  return returnValue;
17
41
  }
42
+
43
+ /**
44
+ * Calculate the difference between two objects, in terms of what keys are the same,
45
+ * what keys are removed, and what keys are added. Keys are sourced deeply in the objects.
46
+ *
47
+ * @param {object} newObj
48
+ * @param {object} oldObj
49
+ * @param {KeyDiffOptions} [options]
50
+ * @returns {KeyDiffResult}
51
+ */
52
+ export function keyDiffDeep(newObj, oldObj, options = {}) {
53
+ const newPaths = flattenPaths(newObj);
54
+ const oldPaths = flattenPaths(oldObj);
55
+ const result = keyDiff(newPaths, oldPaths, options);
56
+ return result;
57
+ }
@@ -1,4 +1,7 @@
1
- import { useDebugMessage } from "./debugMessage";
1
+ import { inspecttransformWalkFn, useDebugMessage } from "./debugMessage";
2
+ import { keyDiffDeep } from "./keyDiff";
3
+ import { transformWalk } from "./transformWalk";
4
+ import { isEqual, partial } from "lodash-es";
2
5
  import {
3
6
  onActivated,
4
7
  onBeforeMount,
@@ -12,8 +15,71 @@ import {
12
15
  onServerPrefetch,
13
16
  onUnmounted,
14
17
  onUpdated,
18
+ unref,
15
19
  } from "vue";
16
20
 
21
+ window.RH_DEBUG_SKIP_EMPTY_CHANGE_EFFECTS = true;
22
+
23
+ export const customHandlers = {
24
+ onRenderTriggered: (debugMessage, e) => {
25
+ const newSeenObjects = new Map();
26
+ const newValue = transformWalk(e.newValue, partial(inspecttransformWalkFn, newSeenObjects));
27
+ const oldSeenObjects = new Map();
28
+ const oldValue = transformWalk(e.oldValue, partial(inspecttransformWalkFn, oldSeenObjects));
29
+ let keyDiffResults;
30
+ if (typeof newValue !== "object" || typeof oldValue !== "object") {
31
+ keyDiffResults = keyDiffDeep(newValue, oldValue);
32
+ keyDiffResults.changedKeys = new Set();
33
+ for (const key of keyDiffResults.sameKeys) {
34
+ // since it is a deep diff, object equality is fine to not trigger a change
35
+ if (newValue[key] !== oldValue[key]) {
36
+ keyDiffResults.changedKeys.add(key);
37
+ }
38
+ }
39
+ delete keyDiffResults.sameKeys;
40
+ for (const key in keyDiffResults) {
41
+ if (keyDiffResults[key].size === 0) {
42
+ delete keyDiffResults[key];
43
+ }
44
+ }
45
+ if (Object.keys(keyDiffResults).length === 0 && window.RH_DEBUG_SKIP_EMPTY_CHANGE_EFFECTS) {
46
+ return;
47
+ }
48
+ } else {
49
+ if (isEqual(newValue, oldValue)) {
50
+ return;
51
+ }
52
+ }
53
+ const debugArgs = [
54
+ {
55
+ effect: e.effect,
56
+ fn: e.effect.fn,
57
+ target: unref(e.target),
58
+ type: e.type,
59
+ key: e.key,
60
+ newValue,
61
+ oldValue,
62
+ oldTarget: unref(e.oldTarget),
63
+ },
64
+ ];
65
+ if (keyDiffResults) {
66
+ debugArgs.push(keyDiffResults);
67
+ }
68
+ debugMessage(...debugArgs);
69
+ },
70
+ onRenderTracked: (debugMessage, e) => {
71
+ debugMessage({
72
+ fn: e.effect.fn,
73
+ target: unref(e.target),
74
+ type: e.type,
75
+ key: e.key,
76
+ });
77
+ },
78
+ };
79
+ const defaultHandler = (debugMessage) => {
80
+ debugMessage();
81
+ };
82
+
17
83
  /**
18
84
  * @param {string[]} categories
19
85
  */
@@ -42,11 +108,9 @@ export function useLifecycleDebug(categories, includes = [], excludes = []) {
42
108
  continue;
43
109
  }
44
110
  const myCategories = new Set(categories);
111
+ myCategories.add("lifecycle");
45
112
  myCategories.add(key);
46
- const eventString = `${key} called`;
47
113
  const debugMessage = useDebugMessage(myCategories);
48
- lifeCycleFns[key](() => {
49
- debugMessage(eventString);
50
- });
114
+ lifeCycleFns[key](partial(customHandlers[key] || defaultHandler, debugMessage));
51
115
  }
52
116
  }
@@ -0,0 +1,52 @@
1
+ import { isSet } from "lodash-es";
2
+
3
+ /**
4
+ * Recursively walks through an object's values and applies a transformation function to each value.
5
+ * The value recursed into is the transformed value, not the original value.
6
+ *
7
+ * @example
8
+ *
9
+ * const obj = {
10
+ * a: 1,
11
+ * b: {
12
+ * c: 2,
13
+ * d: [3, 4, { e: 5 }]
14
+ * }
15
+ * };
16
+ *
17
+ * const transformed = transformWalk(obj, (key, value, path) => {
18
+ * if (key === "e") {
19
+ * return value * 2;
20
+ * }
21
+ * return value;
22
+ * });
23
+ * // transformed = {
24
+ * // a: 1,
25
+ * // b: {
26
+ * // c: 2,
27
+ * // d: [3, 4, { e: 10 }]
28
+ * // }
29
+ * // }
30
+ *
31
+ * @param {*} obj The object to start walking from.
32
+ * @param {function} transformFn The function to transform each value.
33
+ * @param {string} path The path to the current value.
34
+ * @returns {*} The transformed initial value.
35
+ */
36
+ export const transformWalk = (obj, transformFn, path = "") => {
37
+ if (typeof obj !== "object" || obj === null || isSet(obj)) {
38
+ // base case: obj is not an object or is null
39
+ return obj;
40
+ }
41
+ if (Array.isArray(obj)) {
42
+ // recursive case: obj is an array
43
+ return obj.map((item, index) => transformWalk(item, transformFn, path + `[${index}]`));
44
+ }
45
+ // recursive case: obj is an object
46
+ const result = {};
47
+ for (const [key, value] of Object.entries(obj)) {
48
+ const transformedValue = transformFn(key, value, path + `.${key}`);
49
+ result[key] = transformWalk(transformedValue, transformFn, path + `.${key}`);
50
+ }
51
+ return result;
52
+ };