@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.
- package/LICENSE.md +4 -199
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +81 -72
- package/src/encapsulate.ts +175 -25
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +207 -135
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +131 -37
- package/src/spine-factories/CapsuleSpineFactory.v0.ts +53 -6
- package/src/static-analyzer.v0.ts +291 -52
package/src/encapsulate.ts
CHANGED
|
@@ -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
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
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
|
}
|