File size: 3,701 Bytes
cd6720a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { invariant } from 'outvariant'

class PatchesRegistry {
  #replacements = new Map<object, Map<PropertyKey, () => void>>()

  public applyPatch<Owner extends object, K extends keyof Owner>(
    owner: Owner,
    key: K,
    getNextValue: (realValue: Owner[K]) => Owner[K]
  ): () => void {
    const ownerReplacements = this.#replacements.get(owner)

    invariant(
      !ownerReplacements?.has(key),
      `Failed to replace a global value at "${String(key)}": already replaced.`
    )

    const match = getDeepPropertyDescriptor(owner, key)

    if (typeof match === 'undefined') {
      console.warn(
        `Failed to replace a global value at "${String(key)}": not a global value.`
      )
      return () => {}
    }

    if (match.descriptor.configurable) {
      Object.defineProperty(owner, key, {
        value: getNextValue(owner[key]),
        enumerable: true,
        configurable: true,
      })
    } else if (match.descriptor.writable) {
      owner[key] = getNextValue(owner[key])
    } else {
      throw new Error(
        `Failed to patch a non-configurable non-writable property "${key.toString()}"`
      )
    }

    const restorePatch = () => {
      const currentReplacements = this.#replacements.get(owner)

      if (!currentReplacements?.has(key)) {
        return
      }

      if (match.owner === owner) {
        /**
         * @note Restoring non-configurable properties works as long as "writable: true"
         * and none of the other descriptor properties except for "value" have changed.
         */
        Object.defineProperty(match.owner, key, match.descriptor)
      } else {
        /**
         * @todo Delete the proxy property set by the registry.
         * If the match's owner isn't the original owner, the property is likely nested in the prototype.
         * The registry does not meddle with those, they are left intact.
         */
        Reflect.deleteProperty(owner, key)
      }

      currentReplacements.delete(key)

      if (currentReplacements.size === 0) {
        this.#replacements.delete(owner)
      }
    }

    if (ownerReplacements) {
      ownerReplacements.set(key, restorePatch)
    } else {
      this.#replacements.set(owner, new Map([[key, restorePatch]]))
    }

    return restorePatch
  }

  public restoreAllPatches(): void {
    const errors: Array<Error> = []

    for (const [, ownerReplacements] of this.#replacements) {
      for (const [, restorePatch] of ownerReplacements) {
        try {
          restorePatch()
        } catch (error) {
          if (error instanceof Error) {
            errors.push(error)
          } else {
            throw error
          }
        }
      }
    }

    if (errors.length > 0) {
      throw new AggregateError(errors, 'FOO!')
    }
  }
}

export const patchesRegistry = new PatchesRegistry()

interface DeepDescriptorMatch {
  owner: object
  descriptor: PropertyDescriptor
}

/**
 * Returns a property descriptor for the given property on the owner.
 * Walks down the prototype chain if the property does not exist on the owner.
 * Handy for getting a global property descriptor where `globalThis` is
 * replaced with a controlled class (e.g. ServiceWorkerGlobalScope).
 */
export function getDeepPropertyDescriptor<Owner extends object>(
  owner: Owner,
  key: keyof Owner
): DeepDescriptorMatch | undefined {
  let currentOwner: Owner | null = owner
  let descriptor: PropertyDescriptor | undefined

  while (currentOwner) {
    descriptor = Object.getOwnPropertyDescriptor(currentOwner, key)

    if (descriptor) {
      return {
        owner: currentOwner,
        descriptor,
      }
    }

    currentOwner = Object.getPrototypeOf(currentOwner)
  }
}