@stream44.studio/encapsulate 0.2.0-rc.2 → 0.2.0-rc.4

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.

Potentially problematic release.


This version of @stream44.studio/encapsulate might be problematic. Click here for more details.

@@ -1,90 +1,5 @@
1
1
  import { CapsulePropertyTypes } from "../../encapsulate"
2
- import { ContractCapsuleInstanceFactory } from "./Static.v0"
3
-
4
- function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, filepath?: string, line?: number, column?: number }> {
5
- const lines = stack.split('\n')
6
- const result: Array<{ function?: string, filepath?: string, line?: number, column?: number }> = []
7
-
8
- // Skip first line (Error message), then collect ALL frames
9
- for (let i = 1; i < lines.length; i++) {
10
- const line = lines[i].trim()
11
-
12
- // Match various stack trace formats:
13
- // "at functionName (/path/to/file:line:column)"
14
- // "at /path/to/file:line:column"
15
- // "at functionName (file:line:column)"
16
- const match = line.match(/at\s+(.+)/)
17
- if (match) {
18
- const frame: { function?: string, filepath?: string, line?: number, column?: number } = {}
19
- const content = match[1]
20
-
21
- // Try to extract function name and location
22
- const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
23
- if (funcMatch) {
24
- // Has function name: "functionName (/path/to/file:line:column)"
25
- const funcName = funcMatch[1].trim()
26
- // Only include function name if not anonymous
27
- if (funcName !== '<anonymous>' && funcName !== 'async <anonymous>') {
28
- frame.function = funcName
29
- }
30
- const location = funcMatch[2]
31
- const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
32
- if (locMatch) {
33
- frame.filepath = locMatch[1]
34
- frame.line = parseInt(locMatch[2], 10)
35
- frame.column = parseInt(locMatch[3], 10)
36
- }
37
- } else {
38
- // No function name: "/path/to/file:line:column"
39
- const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
40
- if (locMatch) {
41
- frame.filepath = locMatch[1]
42
- frame.line = parseInt(locMatch[2], 10)
43
- frame.column = parseInt(locMatch[3], 10)
44
- }
45
- }
46
-
47
- // Convert absolute paths to relative paths if spineFilesystemRoot is provided
48
- if (frame.filepath && spineFilesystemRoot) {
49
- if (frame.filepath.startsWith(spineFilesystemRoot)) {
50
- frame.filepath = frame.filepath.slice(spineFilesystemRoot.length)
51
- // Remove leading slash if present
52
- if (frame.filepath.startsWith('/')) {
53
- frame.filepath = frame.filepath.slice(1)
54
- }
55
- }
56
- }
57
-
58
- // Include all frames, even if incomplete
59
- if (frame.filepath || frame.function) {
60
- result.push(frame)
61
- }
62
- }
63
- }
64
- return result
65
- }
66
-
67
- function extractCallerInfo(stack: Array<{ function?: string, filepath?: string, line?: number, column?: number }>, offset: number = 0) {
68
- // Use offset to skip frames in the stack
69
- // offset 0 = first frame, offset 1 = second frame, etc.
70
-
71
- if (offset < stack.length) {
72
- const frame = stack[offset]
73
- return {
74
- filepath: frame.filepath,
75
- line: frame.line
76
- }
77
- }
78
-
79
- // Fallback to first frame if offset is out of bounds
80
- if (stack.length > 0) {
81
- return {
82
- filepath: stack[0].filepath,
83
- line: stack[0].line
84
- }
85
- }
86
- return {}
87
- }
2
+ import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static.v0"
88
3
 
89
4
  type CallerContext = {
90
5
  capsuleSourceLineRef: string
@@ -144,13 +59,14 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
144
59
  private encapsulateOptions: any
145
60
  private capsuleSourceNameRef?: string
146
61
  private capsuleSourceNameRefHash?: string
147
- private runtimeSpineContracts?: Record<string, any>
62
+ protected override runtimeSpineContracts?: Record<string, any>
148
63
  public id: string
149
64
 
150
65
  constructor({
151
66
  spineContractUri,
152
67
  capsule,
153
68
  self,
69
+ ownSelf,
154
70
  encapsulatedApi,
155
71
  resolve,
156
72
  importCapsule,
@@ -162,11 +78,14 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
162
78
  getEventIndex,
163
79
  incrementEventIndex,
164
80
  currentCallerContext,
165
- runtimeSpineContracts
81
+ runtimeSpineContracts,
82
+ instanceRegistry,
83
+ extendedCapsuleInstance
166
84
  }: {
167
85
  spineContractUri: string
168
86
  capsule: any
169
87
  self: any
88
+ ownSelf?: any
170
89
  encapsulatedApi: Record<string, any>
171
90
  resolve?: (uri: string, parentFilepath: string) => Promise<string>
172
91
  importCapsule?: (filepath: string) => Promise<any>
@@ -179,8 +98,10 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
179
98
  incrementEventIndex: () => number
180
99
  currentCallerContext?: CallerContext
181
100
  runtimeSpineContracts?: Record<string, any>
101
+ instanceRegistry?: CapsuleInstanceRegistry
102
+ extendedCapsuleInstance?: any
182
103
  }) {
183
- super({ spineContractUri, capsule, self, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule })
104
+ super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance })
184
105
  this.getEventIndex = getEventIndex
185
106
  this.incrementEventIndex = incrementEventIndex
186
107
  this.currentCallerContext = currentCallerContext
@@ -202,9 +123,84 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
202
123
  const mappedCapsule = await this.resolveMappedCapsule({ property })
203
124
  const constants = await this.extractConstants({ mappedCapsule })
204
125
 
205
- const mappingOptions = await property.definition.options?.({
206
- constants
207
- })
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
+ }
208
204
 
209
205
  // Transform overrides if this mapping has a propertyContractDelegate
210
206
  let mappedOverrides = overrides
@@ -237,6 +233,12 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
237
233
  runtimeSpineContracts: this.runtimeSpineContracts
238
234
  })
239
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
+
240
242
  const apiTarget = this.getApiTarget({ property })
241
243
  apiTarget[property.name] = new Proxy(mappedCapsuleInstance, {
242
244
  get: (apiTarget: any, apiProp: string | symbol) => {
@@ -263,6 +265,10 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
263
265
  }
264
266
  }
265
267
 
268
+ // Access through .api if it exists (for capsule instances with getters)
269
+ if (apiTarget.api && apiProp in apiTarget.api) {
270
+ return apiTarget.api[apiProp]
271
+ }
266
272
  return apiTarget[apiProp]
267
273
  }
268
274
  })
@@ -402,27 +408,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
402
408
  }
403
409
 
404
410
  protected mapFunctionProperty({ property }: { property: any }) {
405
-
406
- // Create a proxy for this.self that intercepts property access
407
- // Prefer this.self (which has unwrapped APIs) over encapsulatedApi
408
- const selfProxy = new Proxy(this.self, {
409
- get: (target: any, prop: string | symbol) => {
410
- if (typeof prop === 'symbol') return target[prop]
411
-
412
- // First check if the property exists in target (this.self)
413
- if (prop in target) {
414
- return target[prop]
415
- }
416
-
417
- // Fall back to encapsulatedApi
418
- if (prop in this.encapsulatedApi) {
419
- return this.encapsulatedApi[prop]
420
- }
421
-
422
- return undefined
423
- }
424
- })
425
-
411
+ const selfProxy = this.createSelfProxy()
426
412
  const boundFunction = property.definition.value.bind(selfProxy)
427
413
 
428
414
  const valueKey = `__value_${property.name}`
@@ -481,26 +467,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
481
467
 
482
468
  protected mapGetterFunctionProperty({ property }: { property: any }) {
483
469
  const getterFn = property.definition.value
484
-
485
- // Create a proxy for this.self that intercepts property access
486
- // Prefer this.self (which has unwrapped APIs) over encapsulatedApi
487
- const selfProxy = new Proxy(this.self, {
488
- get: (target: any, prop: string | symbol) => {
489
- if (typeof prop === 'symbol') return target[prop]
490
-
491
- // First check if the property exists in target (this.self)
492
- if (prop in target) {
493
- return target[prop]
494
- }
495
-
496
- // Fall back to encapsulatedApi
497
- if (prop in this.encapsulatedApi) {
498
- return this.encapsulatedApi[prop]
499
- }
500
-
501
- return undefined
502
- }
503
- })
470
+ const selfProxy = this.createSelfProxy()
504
471
 
505
472
  Object.defineProperty(this.encapsulatedApi, property.name, {
506
473
  get: () => {
@@ -532,6 +499,18 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
532
499
  enumerable: true,
533
500
  configurable: true
534
501
  })
502
+
503
+ // Also define the getter on ownSelf so this.self.propertyName works for getter functions
504
+ // This ensures this.self accesses the getter, not a raw value
505
+ if (this.ownSelf) {
506
+ Object.defineProperty(this.ownSelf, property.name, {
507
+ get: () => {
508
+ return getterFn.call(selfProxy)
509
+ },
510
+ enumerable: true,
511
+ configurable: true
512
+ })
513
+ }
535
514
  }
536
515
 
537
516
  private addCallerContextToEvent(event: any): void {
@@ -592,14 +571,17 @@ export function CapsuleSpineContract({
592
571
 
593
572
  let eventIndex = 0
594
573
  let currentCallerContext: CallerContext | undefined = undefined
574
+ const instanceRegistry: CapsuleInstanceRegistry = new Map()
595
575
 
596
576
  return {
597
577
  '#': CapsuleSpineContract['#'],
598
- makeContractCapsuleInstance: ({ encapsulateOptions, spineContractUri, self, capsule, encapsulatedApi, runtimeSpineContracts }: { encapsulateOptions: any, spineContractUri: string, self: any, capsule?: any, encapsulatedApi: Record<string, any>, runtimeSpineContracts?: Record<string, any> }) => {
578
+ instanceRegistry,
579
+ 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 }) => {
599
580
  return new MembraneContractCapsuleInstanceFactory({
600
581
  spineContractUri,
601
582
  capsule,
602
583
  self,
584
+ ownSelf,
603
585
  encapsulatedApi,
604
586
  spineFilesystemRoot,
605
587
  freezeCapsule,
@@ -611,7 +593,9 @@ export function CapsuleSpineContract({
611
593
  getEventIndex: () => eventIndex,
612
594
  incrementEventIndex: () => eventIndex++,
613
595
  currentCallerContext,
614
- runtimeSpineContracts
596
+ runtimeSpineContracts,
597
+ instanceRegistry,
598
+ extendedCapsuleInstance
615
599
  })
616
600
  },
617
601
  hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
@@ -622,3 +606,91 @@ export function CapsuleSpineContract({
622
606
 
623
607
  CapsuleSpineContract['#'] = '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0'
624
608
 
609
+
610
+
611
+
612
+ function parseCallerFromStack(stack: string, spineFilesystemRoot?: string): Array<{ function?: string, filepath?: string, line?: number, column?: number }> {
613
+ const lines = stack.split('\n')
614
+ const result: Array<{ function?: string, filepath?: string, line?: number, column?: number }> = []
615
+
616
+ // Skip first line (Error message), then collect ALL frames
617
+ for (let i = 1; i < lines.length; i++) {
618
+ const line = lines[i].trim()
619
+
620
+ // Match various stack trace formats:
621
+ // "at functionName (/path/to/file:line:column)"
622
+ // "at /path/to/file:line:column"
623
+ // "at functionName (file:line:column)"
624
+ const match = line.match(/at\s+(.+)/)
625
+ if (match) {
626
+ const frame: { function?: string, filepath?: string, line?: number, column?: number } = {}
627
+ const content = match[1]
628
+
629
+ // Try to extract function name and location
630
+ const funcMatch = content.match(/^(.+?)\s+\((.+)\)$/)
631
+ if (funcMatch) {
632
+ // Has function name: "functionName (/path/to/file:line:column)"
633
+ const funcName = funcMatch[1].trim()
634
+ // Only include function name if not anonymous
635
+ if (funcName !== '<anonymous>' && funcName !== 'async <anonymous>') {
636
+ frame.function = funcName
637
+ }
638
+ const location = funcMatch[2]
639
+ const locMatch = location.match(/^(.+):(\d+):(\d+)$/)
640
+ if (locMatch) {
641
+ frame.filepath = locMatch[1]
642
+ frame.line = parseInt(locMatch[2], 10)
643
+ frame.column = parseInt(locMatch[3], 10)
644
+ }
645
+ } else {
646
+ // No function name: "/path/to/file:line:column"
647
+ const locMatch = content.match(/^(.+):(\d+):(\d+)$/)
648
+ if (locMatch) {
649
+ frame.filepath = locMatch[1]
650
+ frame.line = parseInt(locMatch[2], 10)
651
+ frame.column = parseInt(locMatch[3], 10)
652
+ }
653
+ }
654
+
655
+ // Convert absolute paths to relative paths if spineFilesystemRoot is provided
656
+ if (frame.filepath && spineFilesystemRoot) {
657
+ if (frame.filepath.startsWith(spineFilesystemRoot)) {
658
+ frame.filepath = frame.filepath.slice(spineFilesystemRoot.length)
659
+ // Remove leading slash if present
660
+ if (frame.filepath.startsWith('/')) {
661
+ frame.filepath = frame.filepath.slice(1)
662
+ }
663
+ }
664
+ }
665
+
666
+ // Include all frames, even if incomplete
667
+ if (frame.filepath || frame.function) {
668
+ result.push(frame)
669
+ }
670
+ }
671
+ }
672
+ return result
673
+ }
674
+
675
+ function extractCallerInfo(stack: Array<{ function?: string, filepath?: string, line?: number, column?: number }>, offset: number = 0) {
676
+ // Use offset to skip frames in the stack
677
+ // offset 0 = first frame, offset 1 = second frame, etc.
678
+
679
+ if (offset < stack.length) {
680
+ const frame = stack[offset]
681
+ return {
682
+ filepath: frame.filepath,
683
+ line: frame.line
684
+ }
685
+ }
686
+
687
+ // Fallback to first frame if offset is out of bounds
688
+ if (stack.length > 0) {
689
+ return {
690
+ filepath: stack[0].filepath,
691
+ line: stack[0].line
692
+ }
693
+ }
694
+ return {}
695
+ }
696
+