@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.
@@ -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
+