@stream44.studio/encapsulate 0.4.0-rc.27 → 0.4.0-rc.29
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/README.md +1 -1
- package/package.json +1 -1
- package/src/capsule-projectors/CapsuleModuleProjector.v0.ts +12 -12
- package/src/encapsulate.ts +14 -5
- package/src/spine-contracts/CapsuleSpineContract.v0/Membrane.v0.ts +158 -3
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +2 -2
- package/src/spine-contracts/CapsuleSpineContract.v0/Static.v0.ts +10 -4
- package/src/static-analyzer.v0.ts +15 -37
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<td><a href="https://Stream44.Studio"><img src=".o/stream44.studio/assets/Icon-v1.svg" width="42" height="42"></a></td>
|
|
4
4
|
<td><strong><a href="https://Stream44.Studio">Stream44 Studio</a></strong><br/>Open Development Project</td>
|
|
5
5
|
<td>Preview release for community feedback.<br/>Get in touch on <a href="https://discord.gg/9eBcQXEJAN">discord</a>.</td>
|
|
6
|
-
<td>Hand
|
|
6
|
+
<td>Designed by Hand<br/><b>AI assisted Code</a></td>
|
|
7
7
|
</tr>
|
|
8
8
|
</table>
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -174,7 +174,7 @@ export function CapsuleModuleProjector({
|
|
|
174
174
|
for (const [key, potentialCapsule] of Object.entries(capsules)) {
|
|
175
175
|
if (potentialCapsule === capsule) continue
|
|
176
176
|
|
|
177
|
-
const mappedModulePath = potentialCapsule.cst?.source?.
|
|
177
|
+
const mappedModulePath = potentialCapsule.cst?.source?.moduleUri
|
|
178
178
|
|
|
179
179
|
if (mappedModulePath && (
|
|
180
180
|
mappedModulePath === mappingValue ||
|
|
@@ -224,7 +224,7 @@ export function CapsuleModuleProjector({
|
|
|
224
224
|
for (const [key, potentialCapsule] of Object.entries(capsules)) {
|
|
225
225
|
if (potentialCapsule === capsule) continue
|
|
226
226
|
|
|
227
|
-
const mappedModulePath = potentialCapsule.cst?.source?.
|
|
227
|
+
const mappedModulePath = potentialCapsule.cst?.source?.moduleUri
|
|
228
228
|
|
|
229
229
|
if (mappedModulePath && (
|
|
230
230
|
mappedModulePath === mappingValue ||
|
|
@@ -515,7 +515,7 @@ export function CapsuleModuleProjector({
|
|
|
515
515
|
projectingCapsules.add(capsuleId)
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
timing?.record(`Projector: Start projection for ${capsule.cst.source.moduleFilepath}`)
|
|
518
|
+
timing?.record(`Projector: Start projection for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`)
|
|
519
519
|
|
|
520
520
|
// Only project capsules that have the Capsule struct property
|
|
521
521
|
const spineContract = capsule.cst.spineContracts[spineContractUri]
|
|
@@ -571,10 +571,10 @@ export function CapsuleModuleProjector({
|
|
|
571
571
|
if (allProjectedFilesExist) {
|
|
572
572
|
// Restore snapshotValues from cache
|
|
573
573
|
Object.assign(snapshotValues, merge(snapshotValues, cachedData.snapshotData))
|
|
574
|
-
timing?.record(`Projector: Cache HIT for ${capsule.cst.source.moduleFilepath}`)
|
|
574
|
+
timing?.record(`Projector: Cache HIT for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`)
|
|
575
575
|
return true
|
|
576
576
|
} else {
|
|
577
|
-
timing?.record(timing?.chalk?.yellow?.(`Projector: Cache INVALID (projected files missing) for ${capsule.cst.source.moduleFilepath}`))
|
|
577
|
+
timing?.record(timing?.chalk?.yellow?.(`Projector: Cache INVALID (projected files missing) for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`))
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
}
|
|
@@ -588,7 +588,7 @@ export function CapsuleModuleProjector({
|
|
|
588
588
|
|
|
589
589
|
// Check if this capsule has the Capsule struct (meaning it should be projected)
|
|
590
590
|
if (potentialMappedCapsule.cst?.spineContracts?.[spineContractUri]?.propertyContracts?.['#@stream44.studio/encapsulate/structs/Capsule']) {
|
|
591
|
-
// Check if this capsule's
|
|
591
|
+
// Check if this capsule's moduleUri is referenced in any mapping property
|
|
592
592
|
const mappedModulePath = potentialMappedCapsule.cst.source.moduleFilepath
|
|
593
593
|
|
|
594
594
|
for (const [propContractKey, propContract] of Object.entries(spineContract.propertyContracts)) {
|
|
@@ -612,7 +612,7 @@ export function CapsuleModuleProjector({
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
|
-
timing?.record(timing?.chalk?.red?.(`Projector: Cache MISS for ${capsule.cst.source.moduleFilepath}`))
|
|
615
|
+
timing?.record(timing?.chalk?.red?.(`Projector: Cache MISS for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}`))
|
|
616
616
|
} catch (error) {
|
|
617
617
|
// Cache miss or error, proceed with projection
|
|
618
618
|
}
|
|
@@ -633,13 +633,13 @@ export function CapsuleModuleProjector({
|
|
|
633
633
|
|
|
634
634
|
const cstJson = JSON.stringify(targetCapsule.cst, null, 4)
|
|
635
635
|
const crtJson = JSON.stringify(targetCapsule.crt || {}, null, 4)
|
|
636
|
-
const
|
|
636
|
+
const moduleUri = targetCapsule.cst.source.moduleUri
|
|
637
637
|
const importStackLine = targetCapsule.cst.source.importStackLine
|
|
638
638
|
|
|
639
|
-
// Replace importMeta: import.meta with moduleFilepath: '...'
|
|
639
|
+
// Replace importMeta: import.meta with moduleFilepath: '...' (using npm URI)
|
|
640
640
|
expression = expression.replace(
|
|
641
641
|
/importMeta:\s*import\.meta/g,
|
|
642
|
-
`moduleFilepath: '${
|
|
642
|
+
`moduleFilepath: '${moduleUri}'`
|
|
643
643
|
)
|
|
644
644
|
|
|
645
645
|
// Replace importStack: makeImportStack() with importStackLine: ..., crt: {...}, cst: {...}
|
|
@@ -1662,7 +1662,7 @@ ${mappedDefaultExport}
|
|
|
1662
1662
|
await projectionCacheStore.writeFile(cacheFilename, JSON.stringify(cacheData, null, 2))
|
|
1663
1663
|
} catch (error) {
|
|
1664
1664
|
// Cache write error, continue without failing
|
|
1665
|
-
console.warn(`Warning: Failed to write projection cache for ${capsule.cst.source.moduleFilepath}:`, error)
|
|
1665
|
+
console.warn(`Warning: Failed to write projection cache for ${capsule.cst.source.moduleUri || capsule.cst.source.moduleFilepath}:`, error)
|
|
1666
1666
|
}
|
|
1667
1667
|
}
|
|
1668
1668
|
|
|
@@ -1749,7 +1749,7 @@ capsule['#'] = ${JSON.stringify(capsuleName)}
|
|
|
1749
1749
|
await projectionStore.writeFile(projectedPath, capsuleFileContent)
|
|
1750
1750
|
}
|
|
1751
1751
|
} catch (error) {
|
|
1752
|
-
console.warn(`Warning: Failed to write projection cache for capsule ${registryCapsule.cst.source.moduleFilepath}:`, error)
|
|
1752
|
+
console.warn(`Warning: Failed to write projection cache for capsule ${registryCapsule.cst.source.moduleUri || registryCapsule.cst.source.moduleFilepath}:`, error)
|
|
1753
1753
|
}
|
|
1754
1754
|
}
|
|
1755
1755
|
}
|
package/src/encapsulate.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
// CACHE_BUST_VERSION: Increment this whenever CST cache must be invalidated due to structural changes
|
|
3
3
|
// This ensures projected capsules are regenerated when the CST format changes
|
|
4
|
-
const CACHE_BUST_VERSION =
|
|
4
|
+
const CACHE_BUST_VERSION = 22
|
|
5
5
|
|
|
6
6
|
type TSpineOptions = {
|
|
7
7
|
spineFilesystemRoot?: string,
|
|
@@ -505,21 +505,30 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
505
505
|
|
|
506
506
|
if (typeof importStackLine !== 'number') throw new Error(`Could not determine importStackLine from options`)
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
// Temporary filesystem-based ref used only for passing to static analyzer
|
|
509
|
+
const fsBasedRef = `${moduleFilepath}:${importStackLine}`
|
|
509
510
|
|
|
510
511
|
spine.spineOptions.timing?.record(`Encapsulate: Start for ${moduleFilepath}`)
|
|
511
512
|
|
|
512
|
-
const
|
|
513
|
+
const parseResult = await spine.spineOptions.staticAnalyzer?.parseModule({
|
|
513
514
|
spineOptions: spine.spineOptions,
|
|
514
515
|
encapsulateOptions: {
|
|
515
516
|
moduleFilepath,
|
|
516
517
|
importStackLine,
|
|
517
|
-
capsuleSourceLineRef,
|
|
518
|
+
capsuleSourceLineRef: fsBasedRef,
|
|
518
519
|
capsuleName: options.capsuleName,
|
|
519
520
|
ambientReferences: options.ambientReferences,
|
|
520
521
|
cacheBustVersion: CACHE_BUST_VERSION
|
|
521
522
|
}
|
|
522
|
-
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
// Use moduleUri from static analyzer for npm URI-based capsuleSourceLineRef
|
|
526
|
+
const moduleUri = parseResult?.moduleUri
|
|
527
|
+
const capsuleSourceLineRef = moduleUri
|
|
528
|
+
? `${moduleUri}:${importStackLine}`
|
|
529
|
+
: fsBasedRef
|
|
530
|
+
|
|
531
|
+
const { csts, crts } = parseResult || {
|
|
523
532
|
csts: options.cst ? { [capsuleSourceLineRef]: options.cst } : undefined,
|
|
524
533
|
crts: options.crt ? { [capsuleSourceLineRef]: options.crt } : undefined
|
|
525
534
|
}
|
|
@@ -140,11 +140,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
140
140
|
|
|
141
141
|
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
142
142
|
// options can be a function or an object for regular mappings
|
|
143
|
-
// Always pass { self, constants } - self
|
|
143
|
+
// Always pass { self, constants } - self contains full parent self when depends is specified,
|
|
144
|
+
// otherwise just the Capsule metadata struct (moduleFilepath, capsuleName, etc.)
|
|
144
145
|
const optionsFn = property.definition.options
|
|
146
|
+
const capsuleStructKey = '#@stream44.studio/encapsulate/structs/Capsule'
|
|
147
|
+
const minimalSelf = this.self[capsuleStructKey]
|
|
148
|
+
? { [capsuleStructKey]: this.self[capsuleStructKey] }
|
|
149
|
+
: {}
|
|
145
150
|
const mappingOptions = property.definition.delegateOptions
|
|
146
151
|
|| (typeof optionsFn === 'function'
|
|
147
|
-
? await optionsFn({ self: property.definition.depends ? this.self :
|
|
152
|
+
? await optionsFn({ self: property.definition.depends ? this.self : minimalSelf, constants })
|
|
148
153
|
: optionsFn)
|
|
149
154
|
|
|
150
155
|
// Check for existing instance in registry - reuse if available (regardless of options)
|
|
@@ -417,6 +422,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
417
422
|
const event: any = {
|
|
418
423
|
event: 'get',
|
|
419
424
|
eventIndex: this.incrementEventIndex(),
|
|
425
|
+
membrane: 'external',
|
|
420
426
|
target: {
|
|
421
427
|
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
422
428
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -445,6 +451,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
445
451
|
const event: any = {
|
|
446
452
|
event: 'set',
|
|
447
453
|
eventIndex: this.incrementEventIndex(),
|
|
454
|
+
membrane: 'external',
|
|
448
455
|
target: {
|
|
449
456
|
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
450
457
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -511,6 +518,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
511
518
|
const callEvent: any = {
|
|
512
519
|
event: 'call',
|
|
513
520
|
eventIndex: this.incrementEventIndex(),
|
|
521
|
+
membrane: 'external',
|
|
514
522
|
target: {
|
|
515
523
|
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
516
524
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -536,6 +544,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
536
544
|
const resultEvent: any = {
|
|
537
545
|
event: 'call-result',
|
|
538
546
|
eventIndex: this.incrementEventIndex(),
|
|
547
|
+
membrane: 'external',
|
|
539
548
|
callEventIndex: callEvent.eventIndex,
|
|
540
549
|
target: {
|
|
541
550
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -552,6 +561,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
552
561
|
const callEvent: any = {
|
|
553
562
|
event: 'call',
|
|
554
563
|
eventIndex: this.incrementEventIndex(),
|
|
564
|
+
membrane: 'external',
|
|
555
565
|
target: {
|
|
556
566
|
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
557
567
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -588,6 +598,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
588
598
|
const resultEvent: any = {
|
|
589
599
|
event: 'call-result',
|
|
590
600
|
eventIndex: this.incrementEventIndex(),
|
|
601
|
+
membrane: 'external',
|
|
591
602
|
callEventIndex: callEvent.eventIndex,
|
|
592
603
|
target: {
|
|
593
604
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -637,6 +648,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
637
648
|
const event: any = {
|
|
638
649
|
event: 'get',
|
|
639
650
|
eventIndex: this.incrementEventIndex(),
|
|
651
|
+
membrane: 'external',
|
|
640
652
|
target: {
|
|
641
653
|
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
642
654
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -673,6 +685,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
673
685
|
const event: any = {
|
|
674
686
|
event: 'get',
|
|
675
687
|
eventIndex: this.incrementEventIndex(),
|
|
688
|
+
membrane: 'external',
|
|
676
689
|
target: {
|
|
677
690
|
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
678
691
|
spineContractCapsuleInstanceId: this.id,
|
|
@@ -718,6 +731,134 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
718
731
|
}
|
|
719
732
|
}
|
|
720
733
|
|
|
734
|
+
protected override createSelfProxy() {
|
|
735
|
+
const extendedApi = this.extendedCapsuleInstance?.api
|
|
736
|
+
const ownSelf = this.ownSelf
|
|
737
|
+
const factory = this
|
|
738
|
+
return new Proxy(this.self, {
|
|
739
|
+
get: (target: any, prop: string | symbol) => {
|
|
740
|
+
if (typeof prop === 'symbol') return target[prop]
|
|
741
|
+
|
|
742
|
+
// 'self' property returns ownSelf (only this capsule's own properties)
|
|
743
|
+
if (prop === 'self' && ownSelf) {
|
|
744
|
+
return ownSelf
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Determine the value source and get the value
|
|
748
|
+
let value: any
|
|
749
|
+
let source: 'self' | 'encapsulatedApi' | 'childApi' | 'extendedApi' | undefined
|
|
750
|
+
|
|
751
|
+
if (prop in target) {
|
|
752
|
+
value = target[prop]
|
|
753
|
+
source = 'self'
|
|
754
|
+
} else if (prop in factory.encapsulatedApi) {
|
|
755
|
+
value = factory.encapsulatedApi[prop]
|
|
756
|
+
source = 'encapsulatedApi'
|
|
757
|
+
} else if (factory.childEncapsulatedApis) {
|
|
758
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
759
|
+
if (prop in childApi) {
|
|
760
|
+
value = childApi[prop]
|
|
761
|
+
source = 'childApi'
|
|
762
|
+
break
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (source === undefined && extendedApi && prop in extendedApi) {
|
|
768
|
+
value = extendedApi[prop]
|
|
769
|
+
source = 'extendedApi'
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Only emit internal events if we're inside a function/getter execution (caller context is set)
|
|
773
|
+
// and the property is not a function (we don't want to emit get events for function references)
|
|
774
|
+
if (source && typeof value !== 'function' && this.getCurrentCallerContext()) {
|
|
775
|
+
const event: any = {
|
|
776
|
+
event: 'get',
|
|
777
|
+
eventIndex: this.incrementEventIndex(),
|
|
778
|
+
membrane: 'internal',
|
|
779
|
+
target: {
|
|
780
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
781
|
+
spineContractCapsuleInstanceId: this.id,
|
|
782
|
+
prop: prop as string,
|
|
783
|
+
},
|
|
784
|
+
value
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (this.capsuleSourceNameRef) {
|
|
788
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
789
|
+
}
|
|
790
|
+
if (this.capsuleSourceNameRefHash) {
|
|
791
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.addCallerContextToEvent(event)
|
|
795
|
+
this.onMembraneEvent?.(event)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return value
|
|
799
|
+
},
|
|
800
|
+
ownKeys: (target: any) => {
|
|
801
|
+
const keys = new Set<string>(Object.keys(target))
|
|
802
|
+
for (const k of Object.keys(factory.encapsulatedApi)) keys.add(k)
|
|
803
|
+
if (factory.childEncapsulatedApis) {
|
|
804
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
805
|
+
for (const k of Object.keys(childApi)) keys.add(k)
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (extendedApi) {
|
|
809
|
+
for (const k of Object.keys(extendedApi)) keys.add(k)
|
|
810
|
+
}
|
|
811
|
+
return [...keys]
|
|
812
|
+
},
|
|
813
|
+
set: (target: any, prop: string | symbol, value: any) => {
|
|
814
|
+
if (typeof prop === 'symbol') {
|
|
815
|
+
target[prop] = value
|
|
816
|
+
return true
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Emit internal set event if we're inside a function/getter execution
|
|
820
|
+
if (this.getCurrentCallerContext()) {
|
|
821
|
+
const event: any = {
|
|
822
|
+
event: 'set',
|
|
823
|
+
eventIndex: this.incrementEventIndex(),
|
|
824
|
+
membrane: 'internal',
|
|
825
|
+
target: {
|
|
826
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
827
|
+
spineContractCapsuleInstanceId: this.id,
|
|
828
|
+
prop: prop as string,
|
|
829
|
+
},
|
|
830
|
+
value
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (this.capsuleSourceNameRef) {
|
|
834
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
835
|
+
}
|
|
836
|
+
if (this.capsuleSourceNameRefHash) {
|
|
837
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
this.addCallerContextToEvent(event)
|
|
841
|
+
this.onMembraneEvent?.(event)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
target[prop] = value
|
|
845
|
+
return true
|
|
846
|
+
},
|
|
847
|
+
getOwnPropertyDescriptor: (target: any, prop: string | symbol) => {
|
|
848
|
+
if (typeof prop === 'symbol') return Object.getOwnPropertyDescriptor(target, prop)
|
|
849
|
+
if (prop in target) return Object.getOwnPropertyDescriptor(target, prop)
|
|
850
|
+
if (prop in factory.encapsulatedApi) return { configurable: true, enumerable: true, writable: true, value: factory.encapsulatedApi[prop as string] }
|
|
851
|
+
if (factory.childEncapsulatedApis) {
|
|
852
|
+
for (const childApi of factory.childEncapsulatedApis) {
|
|
853
|
+
if (prop in childApi) return { configurable: true, enumerable: true, writable: true, value: childApi[prop as string] }
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (extendedApi && prop in extendedApi) return { configurable: true, enumerable: true, writable: true, value: extendedApi[prop as string] }
|
|
857
|
+
return undefined
|
|
858
|
+
}
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
721
862
|
private addCallerContextToEvent(event: any): void {
|
|
722
863
|
const callerCtx = this.getCurrentCallerContext()
|
|
723
864
|
if (callerCtx) {
|
|
@@ -782,6 +923,20 @@ export function CapsuleSpineContract({
|
|
|
782
923
|
let currentCallerContext: CallerContext | undefined = undefined
|
|
783
924
|
const instanceRegistry: CapsuleInstanceRegistry = new Map()
|
|
784
925
|
|
|
926
|
+
// Re-entrancy guard: suppress event emission while inside an onMembraneEvent callback.
|
|
927
|
+
// This prevents consumers (e.g. JSON.stringify on event.value) from triggering proxy getters
|
|
928
|
+
// that would cause spurious recursive membrane events with wrong caller context and ordering.
|
|
929
|
+
let isEmittingEvent = false
|
|
930
|
+
const guardedOnMembraneEvent = onMembraneEvent ? (event: any) => {
|
|
931
|
+
if (isEmittingEvent) return
|
|
932
|
+
isEmittingEvent = true
|
|
933
|
+
try {
|
|
934
|
+
onMembraneEvent(event)
|
|
935
|
+
} finally {
|
|
936
|
+
isEmittingEvent = false
|
|
937
|
+
}
|
|
938
|
+
} : undefined
|
|
939
|
+
|
|
785
940
|
return {
|
|
786
941
|
'#': CapsuleSpineContract['#'],
|
|
787
942
|
instanceRegistry,
|
|
@@ -796,7 +951,7 @@ export function CapsuleSpineContract({
|
|
|
796
951
|
freezeCapsule,
|
|
797
952
|
resolve,
|
|
798
953
|
importCapsule,
|
|
799
|
-
onMembraneEvent,
|
|
954
|
+
onMembraneEvent: guardedOnMembraneEvent,
|
|
800
955
|
enableCallerStackInference,
|
|
801
956
|
encapsulateOptions,
|
|
802
957
|
getEventIndex: () => eventIndex,
|
|
@@ -308,8 +308,8 @@ prop: {
|
|
|
308
308
|
- **`options`** — forwarded to the mapped capsule. Keys starting with `'#'` target the mapped capsule's own property contracts. Keys without `'#'` are matched against capsule names deeper in the mapping tree (nested capsule-name-targeted options).
|
|
309
309
|
- **`options({ self, constants })`** — when `options` is a function, it receives `{ self, constants }`.
|
|
310
310
|
- `constants` — all `Literal`/`String` values from the mapped capsule's definition.
|
|
311
|
-
- `self` — the
|
|
312
|
-
- **`depends`** — array of sibling property names that must be resolved before this mapping's `options` function runs. Enables `options({ self })` to access already-resolved siblings (e.g. `self.$auth.realm`)
|
|
311
|
+
- `self` — always contains the Capsule metadata struct (`self['#@stream44.studio/encapsulate/structs/Capsule']` with `moduleFilepath`, `capsuleName`, etc.). When `depends` is specified, `self` also contains the full parent capsule's resolved sibling mappings.
|
|
312
|
+
- **`depends`** — array of sibling property names that must be resolved before this mapping's `options` function runs. Enables `options({ self })` to access already-resolved siblings (e.g. `self.$auth.realm`). Can be declared explicitly or auto-injected by the static analyzer when it detects `self.<name>` references in the options function body.
|
|
313
313
|
- **Instance reuse** — named capsules are registered in an instance registry. If a capsule with the same name is mapped multiple times without options, the existing instance is reused via a deferred proxy.
|
|
314
314
|
|
|
315
315
|
Mapped capsules are accessible via `this.<prop>` (unwrapped API) and `api.<prop>` (raw instance with `.api`).
|
|
@@ -129,8 +129,9 @@ export class ContractCapsuleInstanceFactory {
|
|
|
129
129
|
if (!this.spineFilesystemRoot) throw new Error(`'spineFilesystemRoot' not set!`)
|
|
130
130
|
if (!this.importCapsule) throw new Error(`'importCapsule' not set!`)
|
|
131
131
|
|
|
132
|
-
// Use
|
|
133
|
-
|
|
132
|
+
// Use cst.source.moduleFilepath (always filesystem-relative) for path resolution.
|
|
133
|
+
// encapsulateOptions.moduleFilepath may be an npm URI when loaded from projected files.
|
|
134
|
+
const moduleFilepath = this.capsule.cst?.source?.moduleFilepath || this.capsule.encapsulateOptions?.moduleFilepath
|
|
134
135
|
if (!moduleFilepath) throw new Error(`'moduleFilepath' not available on capsule!`)
|
|
135
136
|
|
|
136
137
|
const parentPath = join(this.spineFilesystemRoot, moduleFilepath)
|
|
@@ -197,11 +198,16 @@ export class ContractCapsuleInstanceFactory {
|
|
|
197
198
|
|
|
198
199
|
// delegateOptions is set by encapsulate.ts for property contract delegates
|
|
199
200
|
// options can be a function or an object for regular mappings
|
|
200
|
-
// Always pass { self, constants } - self
|
|
201
|
+
// Always pass { self, constants } - self contains full parent self when depends is specified,
|
|
202
|
+
// otherwise just the Capsule metadata struct (moduleFilepath, capsuleName, etc.)
|
|
201
203
|
const optionsFn = property.definition.options
|
|
204
|
+
const capsuleStructKey = '#@stream44.studio/encapsulate/structs/Capsule'
|
|
205
|
+
const minimalSelf = this.self[capsuleStructKey]
|
|
206
|
+
? { [capsuleStructKey]: this.self[capsuleStructKey] }
|
|
207
|
+
: {}
|
|
202
208
|
const mappingOptions = property.definition.delegateOptions
|
|
203
209
|
|| (typeof optionsFn === 'function'
|
|
204
|
-
? await optionsFn({ self: property.definition.depends ? this.self :
|
|
210
|
+
? await optionsFn({ self: property.definition.depends ? this.self : minimalSelf, constants })
|
|
205
211
|
: optionsFn)
|
|
206
212
|
|
|
207
213
|
// Check for existing instance in registry - reuse if available when no options
|
|
@@ -244,25 +244,13 @@ export function StaticAnalyzer({
|
|
|
244
244
|
|
|
245
245
|
const moduleFilepath = join(spineOptions.spineFilesystemRoot, encapsulateOptions.moduleFilepath)
|
|
246
246
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const npmUri = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
255
|
-
if (npmUri) {
|
|
256
|
-
// Prefix with o/npmjs.com/node_modules/ for external modules
|
|
257
|
-
cacheFilePath = `o/npmjs.com/node_modules/${npmUri}`
|
|
258
|
-
} else {
|
|
259
|
-
// Fallback to normalized path if npm URI construction fails
|
|
260
|
-
cacheFilePath = normalize(encapsulateOptions.moduleFilepath).replace(/^\.\.\//, '').replace(/\.\.\//g, '')
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
// Internal module - use relative path as-is
|
|
264
|
-
cacheFilePath = encapsulateOptions.moduleFilepath
|
|
265
|
-
}
|
|
247
|
+
// Construct npm URI for the module upfront — used for cache paths and CST keys
|
|
248
|
+
const rawModuleUri: string = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot) || encapsulateOptions.moduleFilepath
|
|
249
|
+
// Strip file extension from URI
|
|
250
|
+
const moduleUriWithoutExt = rawModuleUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
251
|
+
|
|
252
|
+
// Cache file path always uses npm URI (never filesystem-relative paths)
|
|
253
|
+
const cacheFilePath = moduleUriWithoutExt
|
|
266
254
|
|
|
267
255
|
const capsuleSourceLineRef = `${cacheFilePath}:${encapsulateOptions.importStackLine}`
|
|
268
256
|
|
|
@@ -297,7 +285,8 @@ export function StaticAnalyzer({
|
|
|
297
285
|
timing?.record(`StaticAnalyzer: Cache HIT for ${encapsulateOptions.moduleFilepath}`)
|
|
298
286
|
return {
|
|
299
287
|
csts: cachedCsts,
|
|
300
|
-
crts: JSON.parse(crtsContent)
|
|
288
|
+
crts: JSON.parse(crtsContent),
|
|
289
|
+
moduleUri: moduleUriWithoutExt
|
|
301
290
|
}
|
|
302
291
|
}
|
|
303
292
|
}
|
|
@@ -369,24 +358,12 @@ export function StaticAnalyzer({
|
|
|
369
358
|
continue
|
|
370
359
|
}
|
|
371
360
|
|
|
372
|
-
|
|
373
|
-
const
|
|
361
|
+
// Use npm URI for all CST references (never filesystem-relative paths)
|
|
362
|
+
const capsuleSourceLineRef = `${moduleUriWithoutExt}:${encapsulateOptions.importStackLine}`
|
|
363
|
+
const capsuleSourceNameRef = encapsulateOptions.capsuleName && `${moduleUriWithoutExt}:${encapsulateOptions.capsuleName}`
|
|
374
364
|
const capsuleSourceNameRefHash = capsuleSourceNameRef && createHash('sha256').update(capsuleSourceNameRef).digest('hex')
|
|
375
365
|
|
|
376
|
-
|
|
377
|
-
let moduleUri: string | null = await constructNpmUri(moduleFilepath, spineOptions.spineFilesystemRoot)
|
|
378
|
-
|
|
379
|
-
// If npm URI construction failed, fall back to moduleFilepath
|
|
380
|
-
if (!moduleUri) {
|
|
381
|
-
moduleUri = encapsulateOptions.moduleFilepath
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Strip file extension from URI
|
|
385
|
-
const moduleUriWithoutExt = moduleUri.replace(/\.(ts|tsx|js|jsx)$/, '')
|
|
386
|
-
const capsuleSourceUriLineRef = `${moduleUriWithoutExt}:${encapsulateOptions.importStackLine}`
|
|
387
|
-
|
|
388
|
-
// Store moduleUri without extension
|
|
389
|
-
moduleUri = moduleUriWithoutExt
|
|
366
|
+
const capsuleSourceUriLineRef = capsuleSourceLineRef
|
|
390
367
|
|
|
391
368
|
// Extract the capsule expression text from the source
|
|
392
369
|
const capsuleExpression = call.getText(sourceFile)
|
|
@@ -407,7 +384,7 @@ export function StaticAnalyzer({
|
|
|
407
384
|
capsuleSourceUriLineRef,
|
|
408
385
|
source: {
|
|
409
386
|
moduleFilepath: encapsulateOptions.moduleFilepath,
|
|
410
|
-
moduleUri,
|
|
387
|
+
moduleUri: moduleUriWithoutExt,
|
|
411
388
|
capsuleName: encapsulateOptions.capsuleName,
|
|
412
389
|
declarationLine,
|
|
413
390
|
importStackLine: encapsulateOptions.importStackLine,
|
|
@@ -760,6 +737,7 @@ export function StaticAnalyzer({
|
|
|
760
737
|
return {
|
|
761
738
|
csts,
|
|
762
739
|
crts,
|
|
740
|
+
moduleUri: moduleUriWithoutExt
|
|
763
741
|
}
|
|
764
742
|
}
|
|
765
743
|
}
|