@stream44.studio/encapsulate 0.4.0-rc.5
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/.dco-signatures +9 -0
- package/.github/workflows/dco.yaml +12 -0
- package/.github/workflows/test.yml +26 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/.o/assets/Hero-Explosion-v0.jpeg +0 -0
- package/DCO.md +34 -0
- package/LICENSE.md +8 -0
- package/README.md +46 -0
- package/package.json +33 -0
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +1725 -0
- package/src/encapsulate.ts +881 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +705 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +28 -0
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +395 -0
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +582 -0
- package/src/spine-factories/TimingObserver.ts +26 -0
- package/src/static-analyzer.v0.ts +1898 -0
- package/structs/Capsule.ts +22 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { CapsulePropertyTypes } from "../../encapsulate"
|
|
2
|
+
import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static.v0"
|
|
3
|
+
|
|
4
|
+
type CallerContext = {
|
|
5
|
+
capsuleSourceLineRef: string
|
|
6
|
+
capsuleSourceNameRef?: string
|
|
7
|
+
spineContractCapsuleInstanceId: string
|
|
8
|
+
capsuleSourceNameRefHash?: string
|
|
9
|
+
prop?: string
|
|
10
|
+
filepath?: string
|
|
11
|
+
line?: number
|
|
12
|
+
stack?: Array<{ function?: string, filepath?: string, line?: number, column?: number }>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function CapsuleMembrane(target: Record<string, any>, hooks?: {
|
|
16
|
+
onGet?: (data: { prop: string, value: any }) => void
|
|
17
|
+
onSet?: (data: { prop: string, value: any }) => void
|
|
18
|
+
onBeforeCall?: (data: { prop: string, args: any[] }) => void
|
|
19
|
+
onAfterCall?: (data: { prop: string, result: any, args: any[] }) => void
|
|
20
|
+
}, callerContext?: CallerContext) {
|
|
21
|
+
return new Proxy(target, {
|
|
22
|
+
get(obj: any, prop: string | symbol) {
|
|
23
|
+
if (typeof prop === 'symbol') return obj[prop]
|
|
24
|
+
|
|
25
|
+
const value = obj[prop]
|
|
26
|
+
hooks?.onGet?.({ prop: prop as string, value })
|
|
27
|
+
|
|
28
|
+
if (typeof value === 'function') {
|
|
29
|
+
return function (this: any, ...args: any[]) {
|
|
30
|
+
hooks?.onBeforeCall?.({ prop: prop as string, args })
|
|
31
|
+
const result = value.apply(this, args)
|
|
32
|
+
hooks?.onAfterCall?.({ prop: prop as string, args, result })
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return value
|
|
38
|
+
},
|
|
39
|
+
set(obj: any, prop: string | symbol, value: any) {
|
|
40
|
+
if (typeof prop === 'symbol') {
|
|
41
|
+
obj[prop] = value
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
hooks?.onSet?.({ prop: prop as string, value })
|
|
46
|
+
obj[prop] = value
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFactory {
|
|
54
|
+
private getEventIndex: () => number
|
|
55
|
+
private incrementEventIndex: () => number
|
|
56
|
+
private currentCallerContext: CallerContext | undefined
|
|
57
|
+
private onMembraneEvent?: (event: any) => void
|
|
58
|
+
private enableCallerStackInference: boolean
|
|
59
|
+
private encapsulateOptions: any
|
|
60
|
+
private capsuleSourceNameRef?: string
|
|
61
|
+
private capsuleSourceNameRefHash?: string
|
|
62
|
+
protected override runtimeSpineContracts?: Record<string, any>
|
|
63
|
+
public id: string
|
|
64
|
+
|
|
65
|
+
constructor({
|
|
66
|
+
spineContractUri,
|
|
67
|
+
capsule,
|
|
68
|
+
self,
|
|
69
|
+
ownSelf,
|
|
70
|
+
encapsulatedApi,
|
|
71
|
+
resolve,
|
|
72
|
+
importCapsule,
|
|
73
|
+
spineFilesystemRoot,
|
|
74
|
+
freezeCapsule,
|
|
75
|
+
onMembraneEvent,
|
|
76
|
+
enableCallerStackInference,
|
|
77
|
+
encapsulateOptions,
|
|
78
|
+
getEventIndex,
|
|
79
|
+
incrementEventIndex,
|
|
80
|
+
currentCallerContext,
|
|
81
|
+
runtimeSpineContracts,
|
|
82
|
+
instanceRegistry,
|
|
83
|
+
extendedCapsuleInstance
|
|
84
|
+
}: {
|
|
85
|
+
spineContractUri: string
|
|
86
|
+
capsule: any
|
|
87
|
+
self: any
|
|
88
|
+
ownSelf?: any
|
|
89
|
+
encapsulatedApi: Record<string, any>
|
|
90
|
+
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
91
|
+
importCapsule?: (filepath: string) => Promise<any>
|
|
92
|
+
spineFilesystemRoot?: string
|
|
93
|
+
freezeCapsule?: (capsule: any) => Promise<any>
|
|
94
|
+
onMembraneEvent?: (event: any) => void
|
|
95
|
+
enableCallerStackInference: boolean
|
|
96
|
+
encapsulateOptions: any
|
|
97
|
+
getEventIndex: () => number
|
|
98
|
+
incrementEventIndex: () => number
|
|
99
|
+
currentCallerContext?: CallerContext
|
|
100
|
+
runtimeSpineContracts?: Record<string, any>
|
|
101
|
+
instanceRegistry?: CapsuleInstanceRegistry
|
|
102
|
+
extendedCapsuleInstance?: any
|
|
103
|
+
}) {
|
|
104
|
+
super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance })
|
|
105
|
+
this.getEventIndex = getEventIndex
|
|
106
|
+
this.incrementEventIndex = incrementEventIndex
|
|
107
|
+
this.currentCallerContext = currentCallerContext
|
|
108
|
+
this.onMembraneEvent = onMembraneEvent
|
|
109
|
+
this.enableCallerStackInference = enableCallerStackInference
|
|
110
|
+
this.encapsulateOptions = encapsulateOptions
|
|
111
|
+
this.capsuleSourceNameRef = capsule?.cst?.capsuleSourceNameRef
|
|
112
|
+
this.capsuleSourceNameRefHash = capsule?.cst?.capsuleSourceNameRefHash
|
|
113
|
+
this.runtimeSpineContracts = runtimeSpineContracts
|
|
114
|
+
this.id = `$${encapsulateOptions.capsuleSourceLineRef}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setCurrentCallerContext(context: CallerContext | undefined): void {
|
|
118
|
+
this.currentCallerContext = context
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
|
|
122
|
+
|
|
123
|
+
const mappedCapsule = await this.resolveMappedCapsule({ property })
|
|
124
|
+
const constants = await this.extractConstants({ mappedCapsule })
|
|
125
|
+
|
|
126
|
+
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
127
|
+
// options can be a function or an object for regular mappings
|
|
128
|
+
const mappingOptions = property.definition.delegateOptions
|
|
129
|
+
|| (typeof property.definition.options === 'function'
|
|
130
|
+
? await property.definition.options({ constants })
|
|
131
|
+
: property.definition.options)
|
|
132
|
+
|
|
133
|
+
// Check for existing instance in registry - reuse if available (regardless of options)
|
|
134
|
+
// Pre-registration with null allows parent capsules to "claim" a slot before child capsules process
|
|
135
|
+
const capsuleName = mappedCapsule.encapsulateOptions?.capsuleName
|
|
136
|
+
|
|
137
|
+
if (capsuleName && this.instanceRegistry) {
|
|
138
|
+
if (this.instanceRegistry.has(capsuleName)) {
|
|
139
|
+
const existingEntry = this.instanceRegistry.get(capsuleName)
|
|
140
|
+
|
|
141
|
+
// If entry is null (pre-registered) or actual instance, and current mapping has no options, use deferred proxy
|
|
142
|
+
if (!mappingOptions) {
|
|
143
|
+
// Use deferred proxy that resolves from registry when accessed
|
|
144
|
+
const apiTarget = this.getApiTarget({ property })
|
|
145
|
+
const registry = this.instanceRegistry
|
|
146
|
+
apiTarget[property.name] = new Proxy({} as any, {
|
|
147
|
+
get: (_target: any, apiProp: string | symbol) => {
|
|
148
|
+
if (typeof apiProp === 'symbol') return undefined
|
|
149
|
+
const resolvedInstance = registry.get(capsuleName)
|
|
150
|
+
if (!resolvedInstance) {
|
|
151
|
+
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.currentCallerContext = {
|
|
155
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
156
|
+
capsuleSourceNameRef: this.capsuleSourceNameRef,
|
|
157
|
+
spineContractCapsuleInstanceId: this.id,
|
|
158
|
+
capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
|
|
159
|
+
prop: apiProp as string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this.enableCallerStackInference) {
|
|
163
|
+
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
164
|
+
if (stackStr) {
|
|
165
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
166
|
+
if (stackFrames.length > 0) {
|
|
167
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
168
|
+
this.currentCallerContext.filepath = callerInfo.filepath
|
|
169
|
+
this.currentCallerContext.line = callerInfo.line
|
|
170
|
+
this.currentCallerContext.stack = stackFrames
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Access through .api if it exists (for capsule instances with getters)
|
|
176
|
+
if (resolvedInstance.api && apiProp in resolvedInstance.api) {
|
|
177
|
+
return resolvedInstance.api[apiProp]
|
|
178
|
+
}
|
|
179
|
+
return resolvedInstance[apiProp]
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
this.self[property.name] = new Proxy({} as any, {
|
|
183
|
+
get: (_target, prop) => {
|
|
184
|
+
if (typeof prop === 'symbol') return undefined
|
|
185
|
+
const resolvedInstance = registry.get(capsuleName)
|
|
186
|
+
if (!resolvedInstance) {
|
|
187
|
+
throw new Error(`Capsule instance not yet resolved: ${capsuleName}`)
|
|
188
|
+
}
|
|
189
|
+
const value = resolvedInstance.api?.[prop]
|
|
190
|
+
if (value && typeof value === 'object' && value.api) {
|
|
191
|
+
return value.api
|
|
192
|
+
}
|
|
193
|
+
return value
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
// If current mapping has options, fall through to create new instance
|
|
199
|
+
} else {
|
|
200
|
+
// Pre-register as null to claim the slot for this capsule
|
|
201
|
+
this.instanceRegistry.set(capsuleName, null)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Transform overrides if this mapping has a propertyContractDelegate
|
|
206
|
+
let mappedOverrides = overrides
|
|
207
|
+
if (property.definition.propertyContractDelegate) {
|
|
208
|
+
|
|
209
|
+
// Extract overrides for the delegate property contract and map them to '#'
|
|
210
|
+
// Try both capsuleSourceLineRef and capsuleName
|
|
211
|
+
const delegateOverrides =
|
|
212
|
+
overrides?.[this.capsule.encapsulateOptions.capsuleSourceLineRef]?.[property.definition.propertyContractDelegate] ||
|
|
213
|
+
(this.capsule.encapsulateOptions.capsuleName && overrides?.[this.capsule.encapsulateOptions.capsuleName]?.[property.definition.propertyContractDelegate])
|
|
214
|
+
|
|
215
|
+
if (delegateOverrides) {
|
|
216
|
+
mappedOverrides = {
|
|
217
|
+
...overrides,
|
|
218
|
+
[mappedCapsule.capsuleSourceLineRef]: {
|
|
219
|
+
'#': delegateOverrides
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (mappedCapsule.encapsulateOptions.capsuleName) {
|
|
223
|
+
mappedOverrides[mappedCapsule.encapsulateOptions.capsuleName] = {
|
|
224
|
+
'#': delegateOverrides
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const mappedCapsuleInstance = await mappedCapsule.makeInstance({
|
|
231
|
+
overrides: mappedOverrides,
|
|
232
|
+
options: mappingOptions,
|
|
233
|
+
runtimeSpineContracts: this.runtimeSpineContracts
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Register the instance (replaces null pre-registration marker)
|
|
237
|
+
// Always register to make instance available for child capsules with deferred proxies
|
|
238
|
+
if (capsuleName && this.instanceRegistry) {
|
|
239
|
+
this.instanceRegistry.set(capsuleName, mappedCapsuleInstance)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.mappedCapsuleInstances.push(mappedCapsuleInstance)
|
|
243
|
+
|
|
244
|
+
const apiTarget = this.getApiTarget({ property })
|
|
245
|
+
apiTarget[property.name] = new Proxy(mappedCapsuleInstance, {
|
|
246
|
+
get: (apiTarget: any, apiProp: string | symbol) => {
|
|
247
|
+
if (typeof apiProp === 'symbol') return apiTarget[apiProp]
|
|
248
|
+
|
|
249
|
+
this.currentCallerContext = {
|
|
250
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
251
|
+
capsuleSourceNameRef: this.capsuleSourceNameRef,
|
|
252
|
+
spineContractCapsuleInstanceId: this.id,
|
|
253
|
+
capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
|
|
254
|
+
prop: apiProp as string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (this.enableCallerStackInference) {
|
|
258
|
+
const stackStr = new Error('[MAPPED_CAPSULE]').stack
|
|
259
|
+
if (stackStr) {
|
|
260
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
261
|
+
if (stackFrames.length > 0) {
|
|
262
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
263
|
+
this.currentCallerContext.filepath = callerInfo.filepath
|
|
264
|
+
this.currentCallerContext.line = callerInfo.line
|
|
265
|
+
this.currentCallerContext.stack = stackFrames
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Access through .api if it exists (for capsule instances with getters)
|
|
271
|
+
if (apiTarget.api && apiProp in apiTarget.api) {
|
|
272
|
+
return apiTarget.api[apiProp]
|
|
273
|
+
}
|
|
274
|
+
return apiTarget[apiProp]
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Wrap unwrapped API in membrane proxy for this.self
|
|
279
|
+
this.self[property.name] = mappedCapsuleInstance.api ? new Proxy(mappedCapsuleInstance.api, {
|
|
280
|
+
get: (target, prop) => {
|
|
281
|
+
if (typeof prop === 'symbol') return target[prop]
|
|
282
|
+
|
|
283
|
+
const value = target[prop]
|
|
284
|
+
// Recursively unwrap nested .api objects
|
|
285
|
+
if (value && typeof value === 'object' && value.api) {
|
|
286
|
+
return value.api
|
|
287
|
+
}
|
|
288
|
+
return value
|
|
289
|
+
}
|
|
290
|
+
}) : mappedCapsuleInstance
|
|
291
|
+
|
|
292
|
+
// If this mapping has a propertyContractDelegate, also mount the mapped capsule's properties
|
|
293
|
+
// to the property contract namespace for direct access
|
|
294
|
+
if (property.definition.propertyContractDelegate) {
|
|
295
|
+
// Create the property contract namespace if it doesn't exist
|
|
296
|
+
if (!this.encapsulatedApi[property.definition.propertyContractDelegate]) {
|
|
297
|
+
this.encapsulatedApi[property.definition.propertyContractDelegate] = {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Get property definitions from the mapped capsule's CST instead of accessing .api
|
|
301
|
+
// This avoids triggering the proxy and firing unwanted membrane events
|
|
302
|
+
const delegateTarget = this.encapsulatedApi[property.definition.propertyContractDelegate]
|
|
303
|
+
const mappedCapsuleCst = mappedCapsule.cst
|
|
304
|
+
const spineContractProperties = mappedCapsuleCst?.spineContracts?.[this.spineContractUri]?.properties
|
|
305
|
+
|
|
306
|
+
if (spineContractProperties) {
|
|
307
|
+
for (const [key, propDef] of Object.entries(spineContractProperties)) {
|
|
308
|
+
// Skip internal properties that start with '#'
|
|
309
|
+
if (key.startsWith('#')) continue
|
|
310
|
+
|
|
311
|
+
// Wrap the property access in a proxy to track membrane events
|
|
312
|
+
Object.defineProperty(delegateTarget, key, {
|
|
313
|
+
get: () => {
|
|
314
|
+
this.currentCallerContext = {
|
|
315
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
316
|
+
capsuleSourceNameRef: this.capsuleSourceNameRef,
|
|
317
|
+
spineContractCapsuleInstanceId: this.id,
|
|
318
|
+
capsuleSourceNameRefHash: this.capsuleSourceNameRefHash,
|
|
319
|
+
prop: key
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (this.enableCallerStackInference) {
|
|
323
|
+
const stackStr = new Error('[PROPERTY_CONTRACT_DELEGATE]').stack
|
|
324
|
+
if (stackStr) {
|
|
325
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
326
|
+
if (stackFrames.length > 0) {
|
|
327
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
328
|
+
this.currentCallerContext.filepath = callerInfo.filepath
|
|
329
|
+
this.currentCallerContext.line = callerInfo.line
|
|
330
|
+
this.currentCallerContext.stack = stackFrames
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Access the actual value from the instance's api
|
|
336
|
+
return mappedCapsuleInstance.api[key]
|
|
337
|
+
},
|
|
338
|
+
enumerable: true,
|
|
339
|
+
configurable: true
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
protected mapLiteralProperty({ property }: { property: any }) {
|
|
347
|
+
|
|
348
|
+
const value = typeof this.self[property.name] !== 'undefined'
|
|
349
|
+
? this.self[property.name]
|
|
350
|
+
: property.definition.value
|
|
351
|
+
|
|
352
|
+
const valueKey = `__value_${property.name}`
|
|
353
|
+
Object.defineProperty(this.encapsulatedApi, valueKey, {
|
|
354
|
+
value: value,
|
|
355
|
+
writable: true,
|
|
356
|
+
enumerable: false,
|
|
357
|
+
configurable: true
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
361
|
+
get: () => {
|
|
362
|
+
// Read from self as authoritative source (functions and StructInit write to self)
|
|
363
|
+
// Fall back to backing store for values set via the API setter
|
|
364
|
+
const currentValue = property.name in this.self
|
|
365
|
+
? this.self[property.name]
|
|
366
|
+
: this.encapsulatedApi[valueKey]
|
|
367
|
+
|
|
368
|
+
const event: any = {
|
|
369
|
+
event: 'get',
|
|
370
|
+
eventIndex: this.incrementEventIndex(),
|
|
371
|
+
target: {
|
|
372
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
373
|
+
spineContractCapsuleInstanceId: this.id,
|
|
374
|
+
prop: property.name,
|
|
375
|
+
},
|
|
376
|
+
value: currentValue
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (this.capsuleSourceNameRef) {
|
|
380
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
381
|
+
}
|
|
382
|
+
if (this.capsuleSourceNameRefHash) {
|
|
383
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this.addCallerContextToEvent(event)
|
|
387
|
+
this.onMembraneEvent?.(event)
|
|
388
|
+
return currentValue
|
|
389
|
+
},
|
|
390
|
+
set: (newValue) => {
|
|
391
|
+
const event: any = {
|
|
392
|
+
event: 'set',
|
|
393
|
+
eventIndex: this.incrementEventIndex(),
|
|
394
|
+
target: {
|
|
395
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
396
|
+
spineContractCapsuleInstanceId: this.id,
|
|
397
|
+
prop: property.name,
|
|
398
|
+
},
|
|
399
|
+
value: newValue
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (this.capsuleSourceNameRef) {
|
|
403
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
404
|
+
}
|
|
405
|
+
if (this.capsuleSourceNameRefHash) {
|
|
406
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
this.addCallerContextToEvent(event)
|
|
410
|
+
this.onMembraneEvent?.(event)
|
|
411
|
+
this.encapsulatedApi[valueKey] = newValue
|
|
412
|
+
this.self[property.name] = newValue
|
|
413
|
+
},
|
|
414
|
+
enumerable: true,
|
|
415
|
+
configurable: true
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
protected mapFunctionProperty({ property }: { property: any }) {
|
|
420
|
+
const selfProxy = this.createSelfProxy()
|
|
421
|
+
const boundFunction = property.definition.value.bind(selfProxy)
|
|
422
|
+
|
|
423
|
+
const valueKey = `__value_${property.name}`
|
|
424
|
+
Object.defineProperty(this.encapsulatedApi, valueKey, {
|
|
425
|
+
value: boundFunction,
|
|
426
|
+
writable: true,
|
|
427
|
+
enumerable: false,
|
|
428
|
+
configurable: true
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
432
|
+
get: () => {
|
|
433
|
+
return (...args: any[]) => {
|
|
434
|
+
const callEvent: any = {
|
|
435
|
+
event: 'call',
|
|
436
|
+
eventIndex: this.incrementEventIndex(),
|
|
437
|
+
target: {
|
|
438
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
439
|
+
spineContractCapsuleInstanceId: this.id,
|
|
440
|
+
prop: property.name,
|
|
441
|
+
},
|
|
442
|
+
args
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (this.capsuleSourceNameRef) {
|
|
446
|
+
callEvent.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
447
|
+
}
|
|
448
|
+
if (this.capsuleSourceNameRefHash) {
|
|
449
|
+
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.addCallerContextToEvent(callEvent)
|
|
453
|
+
this.onMembraneEvent?.(callEvent)
|
|
454
|
+
|
|
455
|
+
const result = boundFunction(...args)
|
|
456
|
+
|
|
457
|
+
const resultEvent: any = {
|
|
458
|
+
event: 'call-result',
|
|
459
|
+
eventIndex: this.incrementEventIndex(),
|
|
460
|
+
callEventIndex: callEvent.eventIndex,
|
|
461
|
+
target: {
|
|
462
|
+
spineContractCapsuleInstanceId: this.id,
|
|
463
|
+
},
|
|
464
|
+
result
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
this.onMembraneEvent?.(resultEvent)
|
|
468
|
+
|
|
469
|
+
return result
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
enumerable: true,
|
|
473
|
+
configurable: true
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
478
|
+
const getterFn = property.definition.value
|
|
479
|
+
const selfProxy = this.createSelfProxy()
|
|
480
|
+
|
|
481
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
482
|
+
get: () => {
|
|
483
|
+
// Call the getter function lazily when accessed with proper this context
|
|
484
|
+
const result = getterFn.call(selfProxy)
|
|
485
|
+
|
|
486
|
+
const event: any = {
|
|
487
|
+
event: 'get',
|
|
488
|
+
eventIndex: this.incrementEventIndex(),
|
|
489
|
+
target: {
|
|
490
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
491
|
+
spineContractCapsuleInstanceId: this.id,
|
|
492
|
+
prop: property.name,
|
|
493
|
+
},
|
|
494
|
+
value: result
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (this.capsuleSourceNameRef) {
|
|
498
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
499
|
+
}
|
|
500
|
+
if (this.capsuleSourceNameRefHash) {
|
|
501
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
this.addCallerContextToEvent(event)
|
|
505
|
+
this.onMembraneEvent?.(event)
|
|
506
|
+
return result
|
|
507
|
+
},
|
|
508
|
+
enumerable: true,
|
|
509
|
+
configurable: true
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
// Also define the getter on ownSelf so this.self.propertyName works for getter functions
|
|
513
|
+
// This ensures this.self accesses the getter, not a raw value
|
|
514
|
+
if (this.ownSelf) {
|
|
515
|
+
Object.defineProperty(this.ownSelf, property.name, {
|
|
516
|
+
get: () => {
|
|
517
|
+
return getterFn.call(selfProxy)
|
|
518
|
+
},
|
|
519
|
+
enumerable: true,
|
|
520
|
+
configurable: true
|
|
521
|
+
})
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private addCallerContextToEvent(event: any): void {
|
|
526
|
+
if (this.currentCallerContext) {
|
|
527
|
+
event.caller = {
|
|
528
|
+
capsuleSourceLineRef: this.currentCallerContext.capsuleSourceLineRef,
|
|
529
|
+
spineContractCapsuleInstanceId: this.currentCallerContext.spineContractCapsuleInstanceId,
|
|
530
|
+
}
|
|
531
|
+
if (this.currentCallerContext.capsuleSourceNameRef) {
|
|
532
|
+
event.caller.capsuleSourceNameRef = this.currentCallerContext.capsuleSourceNameRef
|
|
533
|
+
}
|
|
534
|
+
if (this.currentCallerContext.capsuleSourceNameRefHash) {
|
|
535
|
+
event.caller.capsuleSourceNameRefHash = this.currentCallerContext.capsuleSourceNameRefHash
|
|
536
|
+
}
|
|
537
|
+
if (this.currentCallerContext.prop) {
|
|
538
|
+
event.caller.prop = this.currentCallerContext.prop
|
|
539
|
+
}
|
|
540
|
+
if (this.currentCallerContext.filepath) {
|
|
541
|
+
event.caller.filepath = this.currentCallerContext.filepath
|
|
542
|
+
}
|
|
543
|
+
if (this.currentCallerContext.line) {
|
|
544
|
+
event.caller.line = this.currentCallerContext.line
|
|
545
|
+
}
|
|
546
|
+
if (this.currentCallerContext.stack) {
|
|
547
|
+
event.caller.stack = this.currentCallerContext.stack
|
|
548
|
+
}
|
|
549
|
+
} else if (this.enableCallerStackInference) {
|
|
550
|
+
const stackStr = new Error('[MEMBRANE_EVENT]').stack
|
|
551
|
+
if (stackStr) {
|
|
552
|
+
const stackFrames = parseCallerFromStack(stackStr, this.spineFilesystemRoot)
|
|
553
|
+
if (stackFrames.length > 0) {
|
|
554
|
+
const callerInfo = extractCallerInfo(stackFrames, 3)
|
|
555
|
+
event.caller = {
|
|
556
|
+
...callerInfo,
|
|
557
|
+
stack: stackFrames
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function CapsuleSpineContract({
|
|
566
|
+
onMembraneEvent,
|
|
567
|
+
freezeCapsule,
|
|
568
|
+
enableCallerStackInference = false,
|
|
569
|
+
spineFilesystemRoot,
|
|
570
|
+
resolve,
|
|
571
|
+
importCapsule
|
|
572
|
+
}: {
|
|
573
|
+
onMembraneEvent?: (event: any) => void
|
|
574
|
+
freezeCapsule?: (capsule: any) => Promise<any>
|
|
575
|
+
enableCallerStackInference?: boolean
|
|
576
|
+
spineFilesystemRoot?: string
|
|
577
|
+
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
578
|
+
importCapsule?: (filepath: string) => Promise<any>
|
|
579
|
+
} = {}) {
|
|
580
|
+
|
|
581
|
+
let eventIndex = 0
|
|
582
|
+
let currentCallerContext: CallerContext | undefined = undefined
|
|
583
|
+
const instanceRegistry: CapsuleInstanceRegistry = new Map()
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
'#': CapsuleSpineContract['#'],
|
|
587
|
+
instanceRegistry,
|
|
588
|
+
makeContractCapsuleInstance: ({ encapsulateOptions, spineContractUri, self, ownSelf, capsule, encapsulatedApi, runtimeSpineContracts, extendedCapsuleInstance }: { encapsulateOptions: any, spineContractUri: string, self: any, ownSelf?: any, capsule?: any, encapsulatedApi: Record<string, any>, runtimeSpineContracts?: Record<string, any>, extendedCapsuleInstance?: any }) => {
|
|
589
|
+
return new MembraneContractCapsuleInstanceFactory({
|
|
590
|
+
spineContractUri,
|
|
591
|
+
capsule,
|
|
592
|
+
self,
|
|
593
|
+
ownSelf,
|
|
594
|
+
encapsulatedApi,
|
|
595
|
+
spineFilesystemRoot,
|
|
596
|
+
freezeCapsule,
|
|
597
|
+
resolve,
|
|
598
|
+
importCapsule,
|
|
599
|
+
onMembraneEvent,
|
|
600
|
+
enableCallerStackInference,
|
|
601
|
+
encapsulateOptions,
|
|
602
|
+
getEventIndex: () => eventIndex,
|
|
603
|
+
incrementEventIndex: () => eventIndex++,
|
|
604
|
+
currentCallerContext,
|
|
605
|
+
runtimeSpineContracts,
|
|
606
|
+
instanceRegistry,
|
|
607
|
+
extendedCapsuleInstance
|
|
608
|
+
})
|
|
609
|
+
},
|
|
610
|
+
hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
|
|
611
|
+
return capsuleSnapshot
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, filepath?: string, line?: number, column?: number }> {
|
|
622
|
+
const lines = stack.split('\n')
|
|
623
|
+
const result: Array<{ function?: string, filepath?: string, line?: number, column?: number }> = []
|
|
624
|
+
|
|
625
|
+
// Skip first line (Error message), then collect ALL frames
|
|
626
|
+
for (let i = 1; i < lines.length; i++) {
|
|
627
|
+
const line = lines[i].trim()
|
|
628
|
+
|
|
629
|
+
// Match various stack trace formats:
|
|
630
|
+
// "at functionName (/path/to/file:line:column)"
|
|
631
|
+
// "at /path/to/file:line:column"
|
|
632
|
+
// "at functionName (file:line:column)"
|
|
633
|
+
const match = line.match(/at\s+(.+)/)
|
|
634
|
+
if (match) {
|
|
635
|
+
const frame: { function?: string, filepath?: string, line?: number, column?: number } = {}
|
|
636
|
+
const content = match[1]
|
|
637
|
+
|
|
638
|
+
// Try to extract function name and location
|
|
639
|
+
const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
|
|
640
|
+
if (funcMatch) {
|
|
641
|
+
// Has function name: "functionName (/path/to/file:line:column)"
|
|
642
|
+
const funcName = funcMatch[1].trim()
|
|
643
|
+
// Only include function name if not anonymous
|
|
644
|
+
if (funcName !== '<anonymous>' && funcName !== 'async <anonymous>') {
|
|
645
|
+
frame.function = funcName
|
|
646
|
+
}
|
|
647
|
+
const location = funcMatch[2]
|
|
648
|
+
const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
|
|
649
|
+
if (locMatch) {
|
|
650
|
+
frame.filepath = locMatch[1]
|
|
651
|
+
frame.line = parseInt(locMatch[2], 10)
|
|
652
|
+
frame.column = parseInt(locMatch[3], 10)
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
// No function name: "/path/to/file:line:column"
|
|
656
|
+
const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
|
|
657
|
+
if (locMatch) {
|
|
658
|
+
frame.filepath = locMatch[1]
|
|
659
|
+
frame.line = parseInt(locMatch[2], 10)
|
|
660
|
+
frame.column = parseInt(locMatch[3], 10)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Convert absolute paths to relative paths if spineFilesystemRoot is provided
|
|
665
|
+
if (frame.filepath && spineFilesystemRoot) {
|
|
666
|
+
if (frame.filepath.startsWith(spineFilesystemRoot)) {
|
|
667
|
+
frame.filepath = frame.filepath.slice(spineFilesystemRoot.length)
|
|
668
|
+
// Remove leading slash if present
|
|
669
|
+
if (frame.filepath.startsWith('/')) {
|
|
670
|
+
frame.filepath = frame.filepath.slice(1)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Include all frames, even if incomplete
|
|
676
|
+
if (frame.filepath || frame.function) {
|
|
677
|
+
result.push(frame)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return result
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function extractCallerInfo(stack: Array<{ function?: string, filepath?: string, line?: number, column?: number }>, offset: number = 0) {
|
|
685
|
+
// Use offset to skip frames in the stack
|
|
686
|
+
// offset 0 = first frame, offset 1 = second frame, etc.
|
|
687
|
+
|
|
688
|
+
if (offset < stack.length) {
|
|
689
|
+
const frame = stack[offset]
|
|
690
|
+
return {
|
|
691
|
+
filepath: frame.filepath,
|
|
692
|
+
line: frame.line
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Fallback to first frame if offset is out of bounds
|
|
697
|
+
if (stack.length > 0) {
|
|
698
|
+
return {
|
|
699
|
+
filepath: stack[0].filepath,
|
|
700
|
+
line: stack[0].line
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return {}
|
|
704
|
+
}
|
|
705
|
+
|