@qilin-agent/kylin 0.0.0-stage → 4.0.3

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.
package/src/utils.ts ADDED
@@ -0,0 +1,287 @@
1
+ import { defineProperty } from '@qilin-agent/cosmokit'
2
+ import type { Context, Service } from './index.ts'
3
+
4
+ /** Ordered collection of disposable values with O(1) deletion by value. */
5
+ export class DisposableList<T extends WeakKey> {
6
+ private sn = 0
7
+ private map = new Map<number, T>()
8
+ private weak = new WeakMap<T, number>()
9
+
10
+ get length() {
11
+ return this.map.size
12
+ }
13
+
14
+ push(value: T) {
15
+ const sn = ++this.sn
16
+ this.map.set(sn, value)
17
+ this.weak.set(value, sn)
18
+ return () => this.map.delete(sn)
19
+ }
20
+
21
+ delete(value: T) {
22
+ const sn = this.weak.get(value)
23
+ if (!sn) return false
24
+ return this.map.delete(sn)
25
+ }
26
+
27
+ clear() {
28
+ const values = [...this.map.values()]
29
+ this.map.clear()
30
+ return values.reverse()
31
+ }
32
+
33
+ [Symbol.iterator]() {
34
+ return this.map.values()
35
+ }
36
+
37
+ [Symbol.for('nodejs.util.inspect.custom')]() {
38
+ return [...this]
39
+ }
40
+ }
41
+
42
+ /** Metadata used by traceable proxies to rebind `ctx` and associated services. */
43
+ export interface Tracker {
44
+ associate?: string
45
+ property?: string
46
+ noShadow?: boolean
47
+ }
48
+
49
+ /** Shared symbols used to avoid public property-name collisions. */
50
+ export const symbols = {
51
+ // internal symbols
52
+ shadow: Symbol.for('cordis.shadow'),
53
+ receiver: Symbol.for('cordis.receiver'),
54
+ original: Symbol.for('cordis.original'),
55
+ metadata: Symbol.for('cordis.metadata'),
56
+ initHooks: Symbol.for('cordis.initHooks'),
57
+ checkProto: Symbol.for('cordis.checkProto'),
58
+
59
+ // context symbols
60
+ effect: Symbol.for('cordis.effect') as typeof Context.effect,
61
+ filter: Symbol.for('cordis.filter') as typeof Context.filter,
62
+ isolate: Symbol.for('cordis.isolate') as typeof Context.isolate,
63
+ intercept: Symbol.for('cordis.intercept') as typeof Context.intercept,
64
+
65
+ // service symbols
66
+ init: Symbol.for('cordis.init') as typeof Service.init,
67
+ check: Symbol.for('cordis.check') as typeof Service.check,
68
+ config: Symbol.for('cordis.config') as typeof Service.config,
69
+ invoke: Symbol.for('cordis.invoke') as typeof Service.invoke,
70
+ extend: Symbol.for('cordis.extend') as typeof Service.extend,
71
+ tracker: Symbol.for('cordis.tracker') as typeof Service.tracker,
72
+ resolveConfig: Symbol.for('cordis.resolveConfig') as typeof Service.resolveConfig,
73
+ }
74
+
75
+ const GeneratorFunction = function* () {}.constructor
76
+ const AsyncGeneratorFunction = async function* () {}.constructor
77
+
78
+ /** Return true when a plugin callback should be constructed with `new`. */
79
+ export function isConstructor(func: any): func is new (...args: any) => any {
80
+ // async function or arrow function
81
+ if (!func.prototype) return false
82
+ // generator function or malformed definition
83
+ // we cannot use below check because `mock.fn()` is proxied
84
+ // if (func.prototype.constructor !== func) return false
85
+ if (func instanceof GeneratorFunction) return false
86
+ // polyfilled AsyncGeneratorFunction === Function
87
+ if (AsyncGeneratorFunction !== Function && func instanceof AsyncGeneratorFunction) return false
88
+ return true
89
+ }
90
+
91
+ /** Merge two prototype chains while preserving descriptors from `proto1`. */
92
+ export function joinPrototype(proto1: {}, proto2: {}) {
93
+ if (proto1 === Object.prototype) return proto2
94
+ const result = Object.create(joinPrototype(Object.getPrototypeOf(proto1), proto2))
95
+ for (const key of Reflect.ownKeys(proto1)) {
96
+ Object.defineProperty(result, key, Object.getOwnPropertyDescriptor(proto1, key)!)
97
+ }
98
+ return result
99
+ }
100
+
101
+ /** Return true for non-null objects and functions. */
102
+ export function isObject(value: any): value is {} {
103
+ return value && (typeof value === 'object' || typeof value === 'function')
104
+ }
105
+
106
+ /** Find a property descriptor by walking an object's prototype chain. */
107
+ export function getPropertyDescriptor(target: any, prop: string | symbol) {
108
+ let proto = target
109
+ while (proto) {
110
+ const desc = Reflect.getOwnPropertyDescriptor(proto, prop)
111
+ if (desc) return desc
112
+ proto = Object.getPrototypeOf(proto)
113
+ }
114
+ }
115
+
116
+ /** Wrap services/functions so method calls see the caller's active context. */
117
+ export function getTraceable<T>(ctx: Context, value: T): T {
118
+ if (!isObject(value)) return value
119
+ if (Object.hasOwn(value, symbols.shadow)) {
120
+ return Object.getPrototypeOf(value)
121
+ }
122
+ const tracker = value[symbols.tracker]
123
+ if (!tracker) return value
124
+ return createTraceable(ctx, value, tracker)
125
+ }
126
+
127
+ /** Return a proxy that overlays readonly or writable properties onto a target. */
128
+ export function withProps(target: any, props?: {}) {
129
+ if (!props) return target
130
+ return new Proxy(target, {
131
+ get: (target, prop, receiver) => {
132
+ if (prop in props && prop !== 'constructor') return Reflect.get(props, prop, receiver)
133
+ return Reflect.get(target, prop, receiver)
134
+ },
135
+ set: (target, prop, value, receiver) => {
136
+ if (prop in props && prop !== 'constructor') return Reflect.set(props, prop, value, receiver)
137
+ return Reflect.set(target, prop, value, receiver)
138
+ },
139
+ })
140
+ }
141
+
142
+ function withProp(target: any, prop: string | symbol, value: any) {
143
+ return withProps(target, Object.defineProperty(Object.create(null), prop, {
144
+ value,
145
+ writable: false,
146
+ }))
147
+ }
148
+
149
+ function createShadow(ctx: Context, target: any, property: string | undefined, receiver: any) {
150
+ if (!property) return receiver
151
+ const origin = Reflect.getOwnPropertyDescriptor(target, property)?.value
152
+ if (!origin) return receiver
153
+ return withProp(receiver, property, ctx.extend({ [symbols.shadow]: origin }))
154
+ }
155
+
156
+ function createShadowMethod(ctx: Context, value: any, outer: any, shadow: {}) {
157
+ return new Proxy(value, {
158
+ apply: (target, thisArg, args) => {
159
+ if (thisArg === outer) thisArg = shadow
160
+ return getTraceable(ctx, Reflect.apply(target, thisArg, args))
161
+ },
162
+ })
163
+ }
164
+
165
+ function createTraceable(ctx: Context, value: any, tracker: Tracker) {
166
+ // noShadow services are identity-aware (e.g. logger uses the origin fiber to
167
+ // derive its name): keep the shadow ctx so they can read [symbols.shadow]
168
+ // and resolve the origin. Non-noShadow services strip — their side effects
169
+ // bind to caller, not origin.
170
+ if (ctx[symbols.shadow] && !tracker.noShadow) {
171
+ ctx = Object.getPrototypeOf(ctx)
172
+ }
173
+ const proxy = new Proxy(value, {
174
+ get: (target, prop, receiver) => {
175
+ if (prop === symbols.original) return target
176
+ if (prop === tracker.property) return ctx
177
+ if (typeof prop === 'symbol') {
178
+ return Reflect.get(target, prop, receiver)
179
+ }
180
+ if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
181
+ return Reflect.get(ctx, `${tracker.associate}.${prop}`, withProp(ctx, symbols.receiver, receiver))
182
+ }
183
+ let shadow: any, innerValue: any
184
+ const desc = getPropertyDescriptor(target, prop)
185
+ if (desc && 'value' in desc) {
186
+ innerValue = desc.value
187
+ } else {
188
+ shadow = createShadow(ctx, target, tracker.property, receiver)
189
+ innerValue = Reflect.get(target, prop, shadow)
190
+ }
191
+ const innerTracker = innerValue?.[symbols.tracker]
192
+ if (innerTracker) {
193
+ return createTraceable(ctx, innerValue, innerTracker)
194
+ } else if (!tracker.noShadow && typeof innerValue === 'function') {
195
+ shadow ??= createShadow(ctx, target, tracker.property, receiver)
196
+ return createShadowMethod(ctx, innerValue, receiver, shadow)
197
+ } else {
198
+ return innerValue
199
+ }
200
+ },
201
+ set: (target, prop, value, receiver) => {
202
+ if (prop === symbols.original) return false
203
+ if (prop === tracker.property) return false
204
+ if (typeof prop === 'symbol') {
205
+ return Reflect.set(target, prop, value, receiver)
206
+ }
207
+ if (tracker.associate && ctx.reflect.props[`${tracker.associate}.${prop}`]) {
208
+ return Reflect.set(ctx, `${tracker.associate}.${prop}`, value, withProp(ctx, symbols.receiver, receiver))
209
+ }
210
+ const shadow = createShadow(ctx, target, tracker.property, receiver)
211
+ return Reflect.set(target, prop, value, shadow)
212
+ },
213
+ apply: (target, thisArg, args) => {
214
+ return applyTraceable(proxy, target, thisArg, args)
215
+ },
216
+ })
217
+ return proxy
218
+ }
219
+
220
+ function applyTraceable(proxy: any, value: any, thisArg: any, args: any[]) {
221
+ if (!value[symbols.invoke]) return Reflect.apply(value, thisArg, args)
222
+ return value[symbols.invoke].apply(proxy, args)
223
+ }
224
+
225
+ /** Create a callable service object that dispatches through `symbols.invoke`. */
226
+ export function createCallable(name: string, proto: {}, tracker: Tracker) {
227
+ const self = function (...args: any[]) {
228
+ const proxy = createTraceable(self['ctx'], self, tracker)
229
+ return applyTraceable(proxy, self, this, args)
230
+ }
231
+ defineProperty(self, 'name', name)
232
+ return Object.setPrototypeOf(self, proto)
233
+ }
234
+
235
+ interface StackInfo {
236
+ offset: number
237
+ error: Error
238
+ }
239
+
240
+ function handleError(info: StackInfo, reason: any, getOuterStack: () => string[]): never {
241
+ const innerLines = info.error.stack!.split('\n')
242
+
243
+ // malformed error
244
+ if (typeof reason?.stack !== 'string') {
245
+ const outerError = new Error(reason)
246
+ const lines = outerError.stack!.split('\n')
247
+ lines.splice(1, Infinity, ...getOuterStack())
248
+ outerError.stack = lines.join('\n')
249
+ throw outerError
250
+ }
251
+
252
+ // long stack trace
253
+ const lines: string[] = reason.stack.split('\n')
254
+ let index = lines.indexOf(innerLines[2])
255
+ if (index === -1) throw reason
256
+
257
+ index -= info.offset
258
+ while (index > 0) {
259
+ if (!lines[index - 1].endsWith(' (<anonymous>)')) break
260
+ index -= 1
261
+ }
262
+ lines.splice(index, Infinity, ...getOuterStack())
263
+ reason.stack = lines.join('\n')
264
+ throw reason
265
+ }
266
+
267
+ /** Run a callback and splice outer call-site frames into thrown async errors. */
268
+ export function composeError<T>(callback: (info: StackInfo) => T, getOuterStack = buildOuterStack()): T {
269
+ const info: StackInfo = { offset: 1, error: new Error() }
270
+
271
+ try {
272
+ const result: any = callback(info)
273
+ if (isObject(result) && 'then' in result) {
274
+ return (result as any).then(undefined, (reason) => handleError(info, reason, getOuterStack)) as T
275
+ } else {
276
+ return result
277
+ }
278
+ } catch (reason: any) {
279
+ handleError(info, reason, getOuterStack)
280
+ }
281
+ }
282
+
283
+ /** Capture a lazy stack-frame supplier for later error composition. */
284
+ export function buildOuterStack(offset = 0) {
285
+ const outerError = new Error()
286
+ return () => outerError.stack!.split('\n').slice(3 + offset)
287
+ }