-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgetters.ts
More file actions
263 lines (255 loc) · 8.3 KB
/
getters.ts
File metadata and controls
263 lines (255 loc) · 8.3 KB
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/**
* @fileoverview Lazy-getter primitives + `createConstantsObject`.
*
* `createConstantsObject` is the most-used factory in this module —
* fleet repos use it to assemble frozen settings objects with a mix of
* eager values, lazy-computed values, and internals reachable through
* `kInternalsSymbol`. The `defineLazyGetter*` helpers underneath are
* the building blocks; `createLazyGetter` is the memoizing primitive.
*
* Cycle: `defineLazyGetters` → `defineLazyGetter` → `defineGetter` +
* `createLazyGetter`. All function-only references, no eager top-
* level evaluation between siblings, so ESM tolerates.
*/
import { kInternalsSymbol } from '../constants/sentinels'
import { SetCtor } from '../primordials/map-set'
import {
ObjectDefineProperties,
ObjectDefineProperty,
ObjectFreeze,
ObjectGetOwnPropertyDescriptors,
ObjectHasOwn,
ObjectSetPrototypeOf,
} from '../primordials/object'
import { ReflectOwnKeys } from '../primordials/reflect'
import {
objectEntries,
toSortedObject,
toSortedObjectFromEntries,
} from './sort'
import type {
ConstantsObjectOptions,
GetterDefObj,
LazyGetterRecord,
LazyGetterStats,
} from './types'
/**
* Create a frozen constants object with lazy getters and internal properties.
*
* This function creates an immutable object with:
* - Regular properties from `props`
* - Lazy getters that compute values on first access
* - Internal properties accessible via `kInternalsSymbol`
* - Mixin properties (lower priority, won't override existing)
* - Alphabetically sorted keys for consistency
*
* The resulting object is deeply frozen and cannot be modified.
*
* @param props - Regular properties to include on the object
* @param options_ - Configuration options
* @returns A frozen object with all specified properties
*
* @example
* ```ts
* const config = createConstantsObject(
* { apiUrl: 'https://api.example.com' },
* {
* getters: { client: () => new APIClient() },
* internals: { version: '1.0.0' }
* }
* )
* console.log(config.apiUrl) // 'https://api.example.com'
* console.log(config.client) // APIClient (lazy)
* console.log(config[kInternalsSymbol].version) // '1.0.0'
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function createConstantsObject(
props: object,
options_?: ConstantsObjectOptions | undefined,
): Readonly<object> {
const options = { __proto__: null, ...options_ } as ConstantsObjectOptions
const attributes = ObjectFreeze({
__proto__: null,
getters: options.getters
? ObjectFreeze(
// oxlint-disable-next-line socket/prefer-undefined-over-null -- Object.setPrototypeOf requires `null` for null-prototype objects.
ObjectSetPrototypeOf(toSortedObject(options.getters), null),
)
: undefined,
internals: options.internals
? ObjectFreeze(
// oxlint-disable-next-line socket/prefer-undefined-over-null
ObjectSetPrototypeOf(toSortedObject(options.internals), null),
)
: undefined,
mixin: options.mixin
? ObjectFreeze(
ObjectDefineProperties(
{ __proto__: null },
ObjectGetOwnPropertyDescriptors(options.mixin),
),
)
: undefined,
props: props
? // oxlint-disable-next-line socket/prefer-undefined-over-null
ObjectFreeze(ObjectSetPrototypeOf(toSortedObject(props), null))
: undefined,
})
const lazyGetterStats = ObjectFreeze({
__proto__: null,
initialized: new SetCtor<PropertyKey>(),
})
const object = defineLazyGetters(
{
__proto__: null,
[kInternalsSymbol]: ObjectFreeze({
__proto__: null,
get attributes() {
return attributes
},
get lazyGetterStats() {
return lazyGetterStats
},
...attributes.internals,
}),
kInternalsSymbol,
...attributes.props,
},
attributes.getters,
lazyGetterStats,
)
if (attributes.mixin) {
ObjectDefineProperties(
object,
toSortedObjectFromEntries(
objectEntries(ObjectGetOwnPropertyDescriptors(attributes.mixin)).filter(
p => !ObjectHasOwn(object, p[0]),
),
) as PropertyDescriptorMap,
)
}
return ObjectFreeze(object)
}
/**
* Create a lazy getter function that memoizes its result.
*
* The returned function will only call the getter once, caching the result
* for subsequent calls. This is useful for expensive computations or
* operations that should only happen when needed.
*
* @param name - The property key name for the getter (used for debugging and stats)
* @param getter - Function that computes the value on first access
* @param stats - Optional stats object to track initialization
* @returns A memoized getter function
*
* @example
* ```ts
* const stats = { initialized: new Set() }
* const getLargeData = createLazyGetter('data', () => expensive(), stats)
* getLargeData() // Computes and caches
* getLargeData() // Returns cached
* stats.initialized.has('data') // true
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export function createLazyGetter<T>(
name: PropertyKey,
getter: () => T,
stats?: LazyGetterStats | undefined,
): () => T {
// Use a unique sentinel object so memoization works even when the
// getter legitimately returns `undefined`. A shared sentinel value
// (like `UNDEFINED_TOKEN === undefined`) would cause repeated calls
// for getters whose result is `undefined`.
const UNCOMPUTED = {}
let lazyValue: T | typeof UNCOMPUTED = UNCOMPUTED
// Dynamically name the getter without using Object.defineProperty.
const { [name]: lazyGetter } = {
[name]() {
if (lazyValue === UNCOMPUTED) {
stats?.initialized?.add(name)
lazyValue = getter()
}
return lazyValue as T
},
} as LazyGetterRecord<T>
return lazyGetter as unknown as () => T
}
/**
* Define a getter property on an object.
*
* The getter is non-enumerable and configurable, meaning it won't show up
* in `for...in` loops or `Object.keys()`, but can be redefined later.
*
* @param object - The object to define the getter on
* @param propKey - The property key for the getter
* @param getter - Function that computes the property value
* @returns The modified object (for chaining)
*
* @example
* ```ts
* const obj = {}
* defineGetter(obj, 'timestamp', () => Date.now())
* obj.timestamp // Current timestamp (computed each access)
* Object.keys(obj) // [] (non-enumerable)
* ```
*/
export function defineGetter<T>(
object: object,
propKey: PropertyKey,
getter: () => T,
): object {
ObjectDefineProperty(object, propKey, {
get: getter,
enumerable: false,
configurable: true,
})
return object
}
/**
* Define a lazy getter property on an object.
*
* Unlike `defineGetter()`, this version memoizes the result so the getter
* function is only called once. Subsequent accesses return the cached value.
*
* @param object - The object to define the lazy getter on
* @param propKey - The property key for the lazy getter
* @param getter - Function that computes the value on first access
* @param stats - Optional stats object to track initialization
* @returns The modified object (for chaining)
*/
export function defineLazyGetter<T>(
object: object,
propKey: PropertyKey,
getter: () => T,
stats?: LazyGetterStats | undefined,
): object {
return defineGetter(object, propKey, createLazyGetter(propKey, getter, stats))
}
/**
* Define multiple lazy getter properties on an object.
*
* Each getter in the provided object will be converted to a lazy getter
* and attached to the target object. All getters share the same stats object
* for tracking initialization.
*
* @param object - The object to define lazy getters on
* @param getterDefObj - Object mapping property keys to getter functions
* @param stats - Optional stats object to track initialization
* @returns The modified object (for chaining)
*/
export function defineLazyGetters(
object: object,
getterDefObj: GetterDefObj | undefined,
stats?: LazyGetterStats | undefined,
): object {
if (getterDefObj !== null && typeof getterDefObj === 'object') {
const keys = ReflectOwnKeys(getterDefObj)
for (let i = 0, { length } = keys; i < length; i += 1) {
const key = keys[i] as PropertyKey
defineLazyGetter(object, key, getterDefObj[key] as () => unknown, stats)
}
}
return object
}