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

@@ -43,7 +43,8 @@ type TCapsuleSnapshot = {
43
43
  type TCapsuleMakeInstanceOptions = {
44
44
  overrides?: Record<string, any>,
45
45
  options?: Record<string, any>,
46
- runtimeSpineContracts?: Record<string, any>
46
+ runtimeSpineContracts?: Record<string, any>,
47
+ sharedSelf?: Record<string, any>
47
48
  }
48
49
 
49
50
  type TCapsule = {
@@ -69,6 +70,7 @@ type TCapsuleOptions = {
69
70
  moduleFilepath?: string,
70
71
  capsuleName?: string,
71
72
  ambientReferences?: Record<string, any>,
73
+ extendsCapsule?: TCapsule | string,
72
74
  cst?: any,
73
75
  crt?: any
74
76
  }
@@ -78,6 +80,7 @@ type TEncapsulateOptions = {
78
80
  importStackLine: number,
79
81
  capsuleName?: string,
80
82
  ambientReferences?: Record<string, any>,
83
+ extendsCapsule?: TCapsule | string,
81
84
  capsuleSourceLineRef: string
82
85
  }
83
86
 
@@ -165,11 +168,15 @@ export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpin
165
168
 
166
169
  let value = target[prop]
167
170
 
168
- // If the value is a Proxy (from Membrane), get the actual value through it
169
- // The Proxy will return the correct value from its get trap
170
- if (value && typeof value === 'object') {
171
- // Check if this value has .api - if so, unwrap it
172
- if (value.api && typeof value.api === 'object') {
171
+ // If the value is a raw capsule instance (has spineContractCapsuleInstances
172
+ // but is NOT a Proxy that handles API access), unwrap it
173
+ // Static.v0 sets apiTarget[property.name] = mappedInstance (raw)
174
+ // Membrane.v0 sets apiTarget[property.name] = new Proxy(...) which handles API access
175
+ if (value && typeof value === 'object' && value.spineContractCapsuleInstances) {
176
+ // Check if this is a raw capsule instance by seeing if it has .api
177
+ // and the .api doesn't have the same spineContractCapsuleInstances
178
+ // (Membrane Proxy would return .api properties, not the raw structure)
179
+ if (value.api && typeof value.api === 'object' && !value.api.spineContractCapsuleInstances) {
173
180
  return createUnwrappingProxy(value)
174
181
  }
175
182
  }
@@ -348,6 +355,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
348
355
  importStackLine,
349
356
  capsuleName: options.capsuleName,
350
357
  ambientReferences: options.ambientReferences,
358
+ extendsCapsule: options.extendsCapsule,
351
359
  capsuleSourceLineRef: `${moduleFilepath}:${importStackLine}`
352
360
  }
353
361
 
@@ -378,30 +386,29 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
378
386
  encapsulateOptions,
379
387
  cst: csts?.[encapsulateOptions.capsuleSourceLineRef],
380
388
  crt: crts?.[encapsulateOptions.capsuleSourceLineRef],
381
- makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts }: TCapsuleMakeInstanceOptions = {}) => {
389
+ makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts, sharedSelf }: TCapsuleMakeInstanceOptions = {}) => {
382
390
 
383
391
  // Create cache key based on parameters
384
- const cacheKey = JSON.stringify({
392
+ // When sharedSelf is provided, we must NOT cache because each extending capsule
393
+ // needs its own instance with its own 'this' context (sharedSelf).
394
+ // This is critical for the pattern where multiple structs extend the same parent.
395
+ const cacheKey = sharedSelf ? null : JSON.stringify({
385
396
  overrides,
386
397
  options,
387
398
  hasRuntimeContracts: !!runtimeSpineContracts
388
399
  })
389
400
 
390
401
  // Check if we already have a pending or completed instance creation
391
- if (instanceCache.has(cacheKey)) {
402
+ // Skip cache when sharedSelf is provided (cacheKey is null)
403
+ if (cacheKey && instanceCache.has(cacheKey)) {
392
404
  return instanceCache.get(cacheKey)!
393
405
  }
394
406
 
395
- // Create the instance promise and cache it immediately
407
+ // Create the instance promise and cache it immediately (only if cacheKey is set)
396
408
  const instancePromise = (async () => {
397
409
  const encapsulatedApi: Record<string, any> = {}
398
410
  const spineContractCapsuleInstances: Record<string, any> = {}
399
411
 
400
- const capsuleInstance = {
401
- api: encapsulatedApi,
402
- spineContractCapsuleInstances
403
- }
404
-
405
412
  // Property contracts are keys starting with '#' that contain nested properties
406
413
  // Structure: spineContractUri -> propertyContractUri -> propertyName -> propertyDef
407
414
  const propertyContractDefinitions: Record<string, Record<string, Record<string, any>>> = {}
@@ -431,7 +438,11 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
431
438
  if (propContractUri !== '#') {
432
439
  // We have a property contract URI we need to resolve and load
433
440
  // Add a dynamic property mapping for this contract to the '#' group
434
- const contractKey = '#' + propContractUri.substring(1)
441
+ // Check if 'as' is defined to use as the property name alias
442
+ const propDefTyped = propDef as Record<string, any>
443
+ const aliasName = propDefTyped.as
444
+ const delegateOptions = propDefTyped.options
445
+ const contractKey = aliasName || ('#' + propContractUri.substring(1))
435
446
 
436
447
  if (!propertyContractDefinitions[spineContractUri]['#']) {
437
448
  propertyContractDefinitions[spineContractUri]['#'] = {}
@@ -440,7 +451,10 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
440
451
  propertyContractDefinitions[spineContractUri]['#'][contractKey] = {
441
452
  type: CapsulePropertyTypes.Mapping,
442
453
  value: propContractUri.substring(1),
443
- propertyContractDelegate: propContractUri
454
+ propertyContractDelegate: propContractUri,
455
+ as: aliasName,
456
+ // Pass options from the property contract delegate to the mapped capsule
457
+ delegateOptions
444
458
  }
445
459
  }
446
460
 
@@ -482,12 +496,120 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
482
496
  }
483
497
  mergeByContract(options, 'Options')
484
498
 
499
+ // Extract default values from property definitions (Literal/String types)
500
+ // This ensures child capsule's default values are available before parent is instantiated
501
+ const defaultPropertyValues: Record<string, any> = {}
502
+ for (const [spineContractUri, propertyContracts] of Object.entries(propertyContractDefinitions)) {
503
+ for (const [propertyContractUri, properties] of Object.entries(propertyContracts)) {
504
+ if (propertyContractUri !== '#') continue
505
+ for (const [propertyName, propertyDef] of Object.entries(properties as Record<string, any>)) {
506
+ if (propertyDef.type === CapsulePropertyTypes.Literal ||
507
+ propertyDef.type === CapsulePropertyTypes.String) {
508
+ if (propertyDef.value !== undefined) {
509
+ defaultPropertyValues[propertyName] = propertyDef.value
510
+ }
511
+ }
512
+ }
513
+ }
514
+ }
515
+
485
516
  // Create a single shared self for all spine contracts by flattening merged values
486
- const self = merge(
487
- {},
488
- defaultInstance,
489
- ...Object.values(mergedValuesByContract)
490
- )
517
+ // ownValues contains this capsule's defaults and overrides
518
+ const ownValues = merge({}, defaultInstance, defaultPropertyValues, ...Object.values(mergedValuesByContract))
519
+
520
+ // If sharedSelf is provided (from extending capsule), we need to:
521
+ // 1. Add parent's properties that child doesn't have
522
+ // 2. Keep child's values for properties that exist in both
523
+ // We do this by assigning parent's values first, then child's values on top
524
+ let self: any
525
+ if (sharedSelf) {
526
+ // Save child's current values (only non-undefined values)
527
+ const childValues: Record<string, any> = {}
528
+ for (const [key, value] of Object.entries(sharedSelf)) {
529
+ if (value !== undefined) {
530
+ childValues[key] = value
531
+ }
532
+ }
533
+ // Assign parent's defaults to sharedSelf (for properties child doesn't have)
534
+ for (const [key, value] of Object.entries(ownValues)) {
535
+ if (!(key in childValues)) {
536
+ sharedSelf[key] = value
537
+ }
538
+ }
539
+ self = sharedSelf
540
+ } else {
541
+ self = ownValues
542
+ }
543
+
544
+ // Create ownSelf containing only this capsule's own properties (not from extends chain)
545
+ // This allows functions to access this.self for their own capsule's properties
546
+ // The selfProxy in spine contracts will expose this as 'self' property
547
+ const ownSelf = merge({}, defaultInstance, defaultPropertyValues, ...Object.values(mergedValuesByContract))
548
+
549
+ // Initialize extended capsule instance if this capsule extends another
550
+ // Pass our self so extended capsule's functions bind to the same context
551
+ let extendedCapsuleInstance: any = undefined
552
+
553
+ // Check CST first, then fall back to encapsulateOptions for direct capsule references
554
+ let extendsCapsuleValue = capsule.cst?.source?.extendsCapsule || encapsulateOptions.extendsCapsule
555
+
556
+ // If extendsCapsule is a string identifier, check if it's in ambientReferences first
557
+ if (typeof extendsCapsuleValue === 'string') {
558
+ const ambientRefs = capsule.cst?.source?.ambientReferences || encapsulateOptions.ambientReferences || {}
559
+ for (const [refName, ref] of Object.entries(ambientRefs)) {
560
+ const refTyped = ref as any
561
+ if (refName === extendsCapsuleValue && refTyped.type === 'capsule' && refTyped.value) {
562
+ extendsCapsuleValue = refTyped.value
563
+ break
564
+ }
565
+ }
566
+ }
567
+
568
+ if (extendsCapsuleValue) {
569
+ let extendsCapsule = extendsCapsuleValue
570
+
571
+ // If it's a string, resolve it using the same mechanism as mappings
572
+ if (typeof extendsCapsule === 'string') {
573
+ // Use the first available spine contract to resolve the URI
574
+ const activeSpineContracts = runtimeSpineContracts || spine.spineContracts
575
+ const firstSpineContractKey = Object.keys(activeSpineContracts)[0]
576
+ const firstSpineContract = activeSpineContracts[firstSpineContractKey] as any
577
+
578
+ if (!firstSpineContract) throw new Error(`No spine contracts available to resolve extendsCapsule URI!`)
579
+
580
+ // Create a contract instance to use resolveMappedCapsule
581
+ const contractInstance = firstSpineContract.makeContractCapsuleInstance({
582
+ spineContractUri: firstSpineContractKey,
583
+ encapsulateOptions,
584
+ capsuleInstance: { api: {}, spineContractCapsuleInstances: {} },
585
+ self: {},
586
+ capsule,
587
+ encapsulatedApi: {},
588
+ runtimeSpineContracts
589
+ })
590
+
591
+ // Resolve using the same mechanism as mappings
592
+ extendsCapsule = await contractInstance.resolveMappedCapsule({
593
+ property: {
594
+ name: '__extends__',
595
+ definition: { value: extendsCapsule }
596
+ }
597
+ })
598
+ }
599
+
600
+ extendedCapsuleInstance = await extendsCapsule.makeInstance({
601
+ overrides,
602
+ options,
603
+ runtimeSpineContracts,
604
+ sharedSelf: self
605
+ })
606
+ }
607
+
608
+ const capsuleInstance = {
609
+ api: encapsulatedApi,
610
+ spineContractCapsuleInstances,
611
+ extendedCapsuleInstance
612
+ }
491
613
 
492
614
  // Use runtime spine contracts if provided, otherwise fall back to encapsulation spine contracts
493
615
  const activeSpineContracts = runtimeSpineContracts || spine.spineContracts
@@ -503,9 +625,11 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
503
625
  encapsulateOptions,
504
626
  capsuleInstance,
505
627
  self,
628
+ ownSelf,
506
629
  capsule,
507
630
  encapsulatedApi,
508
- runtimeSpineContracts
631
+ runtimeSpineContracts,
632
+ extendedCapsuleInstance
509
633
  })
510
634
 
511
635
  spineContractCapsuleInstances[spineContractUri] = spineContractCapsuleInstance
@@ -533,11 +657,37 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
533
657
  }
534
658
  }
535
659
 
660
+ // Wrap encapsulatedApi in a proxy that delegates to extended capsule's API for missing properties
661
+ if (extendedCapsuleInstance) {
662
+ capsuleInstance.api = new Proxy(encapsulatedApi, {
663
+ get: (target: any, prop: string | symbol) => {
664
+ if (typeof prop === 'symbol') return target[prop]
665
+
666
+ // First check if the property exists in local API
667
+ if (prop in target) {
668
+ return target[prop]
669
+ }
670
+
671
+ // Fall back to extended capsule's API
672
+ if (prop in extendedCapsuleInstance.api) {
673
+ return extendedCapsuleInstance.api[prop]
674
+ }
675
+
676
+ return undefined
677
+ },
678
+ has: (target: any, prop: string | symbol) => {
679
+ return prop in target || prop in extendedCapsuleInstance.api
680
+ }
681
+ })
682
+ }
683
+
536
684
  return capsuleInstance
537
685
  })()
538
686
 
539
- // Cache the promise
540
- instanceCache.set(cacheKey, instancePromise)
687
+ // Cache the promise only if cacheKey is set (not when sharedSelf is provided)
688
+ if (cacheKey) {
689
+ instanceCache.set(cacheKey, instancePromise)
690
+ }
541
691
 
542
692
  return instancePromise
543
693
  }