@stream44.studio/encapsulate 0.4.0-rc.38 → 0.4.0-rc.42
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 +2 -2
- package/package.json +4 -4
- package/src/capsule-projectors/{CapsuleModuleProjector.v0.ts → CapsuleModuleProjector.ts} +3 -3
- package/src/encapsulate.ts +89 -28
- package/src/spine-contracts/CapsuleSpineContract.v0/{Membrane.v0.ts → Membrane.ts} +177 -22
- package/src/spine-contracts/CapsuleSpineContract.v0/README.md +72 -4
- package/src/spine-contracts/CapsuleSpineContract.v0/{Static.v0.ts → Static.ts} +235 -28
- package/src/spine-factories/{CapsuleSpineFactory.v0.ts → CapsuleSpineFactory.ts} +172 -39
- package/src/spine-factories/TimingObserver.ts +24 -14
- package/src/{static-analyzer.v0.ts → static-analyzer.ts} +31 -4
- package/structs/Capsule.ts +5 -0
- package/structs/StructFactory.ts +90 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<table>
|
|
2
2
|
<tr>
|
|
3
|
-
<td><a href="https://Stream44.
|
|
4
|
-
<td><strong><a href="https://Stream44.
|
|
3
|
+
<td><a href="https://Stream44.Systems"><img src=".o/stream44.studio/assets/Icon-v1.svg" width="42" height="42"></a></td>
|
|
4
|
+
<td><strong><a href="https://Stream44.Systems">Stream44 Systems</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
6
|
<td>Designed by Hand<br/><b>AI assisted Code</a></td>
|
|
7
7
|
</tr>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream44.studio/encapsulate",
|
|
3
|
-
"version": "0.4.0-rc.
|
|
3
|
+
"version": "0.4.0-rc.42",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"exports": {
|
|
12
12
|
"./encapsulate": "./src/encapsulate.ts",
|
|
13
|
-
"./spine-contracts/CapsuleSpineContract.v0/Static
|
|
14
|
-
"./spine-contracts/CapsuleSpineContract.v0/Membrane
|
|
15
|
-
"./spine-factories/CapsuleSpineFactory
|
|
13
|
+
"./spine-contracts/CapsuleSpineContract.v0/Static": "./src/spine-contracts/CapsuleSpineContract.v0/Static.ts",
|
|
14
|
+
"./spine-contracts/CapsuleSpineContract.v0/Membrane": "./src/spine-contracts/CapsuleSpineContract.v0/Membrane.ts",
|
|
15
|
+
"./spine-factories/CapsuleSpineFactory": "./src/spine-factories/CapsuleSpineFactory.ts",
|
|
16
16
|
"./spine-factories/TimingObserver": "./src/spine-factories/TimingObserver.ts",
|
|
17
17
|
"./structs/Capsule": "./structs/Capsule.ts",
|
|
18
18
|
"./structs/CapsuleProjectionContext": "./structs/CapsuleProjectionContext.ts"
|
|
@@ -50,7 +50,7 @@ async function constructCacheFilePath(moduleFilepath: string, importStackLine: n
|
|
|
50
50
|
* Finds the nearest package.json and constructs an npm URI for cache files.
|
|
51
51
|
* First checks if the path contains /node_modules/ and if so, extracts the portion
|
|
52
52
|
* after the last /node_modules/ occurrence for consistent paths in dev and installed mode.
|
|
53
|
-
* This matches the logic from static-analyzer.
|
|
53
|
+
* This matches the logic from static-analyzer.ts
|
|
54
54
|
*/
|
|
55
55
|
async function constructNpmUriForCache(absoluteFilepath: string, spineRoot: string): Promise<string | null> {
|
|
56
56
|
// Check for /node_modules/ in the path — use the last occurrence to handle nested node_modules
|
|
@@ -742,7 +742,7 @@ export function CapsuleModuleProjector({
|
|
|
742
742
|
const hasStandalone = hasStandaloneProperty(capsule, spineContractUri)
|
|
743
743
|
|
|
744
744
|
// Add runtime imports for standalone functions
|
|
745
|
-
const runtimeImport = hasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane
|
|
745
|
+
const runtimeImport = hasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane'\n` : ''
|
|
746
746
|
|
|
747
747
|
// Generate default export based on capsule type
|
|
748
748
|
let defaultExport = ''
|
|
@@ -1072,7 +1072,7 @@ ${defaultExport}
|
|
|
1072
1072
|
const mappedCapsuleExpression = rewriteCapsuleExpressionWithCST(mappedCapsule)
|
|
1073
1073
|
|
|
1074
1074
|
// Add runtime imports for standalone functions
|
|
1075
|
-
const mappedRuntimeImport = mappedHasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane
|
|
1075
|
+
const mappedRuntimeImport = mappedHasStandalone ? `"use client"\nimport { Spine, SpineRuntime } from '@stream44.studio/encapsulate/encapsulate'\nimport { CapsuleSpineContract } from '@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0/Membrane'\n` : ''
|
|
1076
1076
|
|
|
1077
1077
|
let mappedDefaultExport = ''
|
|
1078
1078
|
if (mappedHasStandalone) {
|
package/src/encapsulate.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
|
|
2
|
+
import type { TimingObserverInterface } from './spine-factories/TimingObserver'
|
|
3
|
+
|
|
2
4
|
// CACHE_BUST_VERSION: Increment this whenever CST cache must be invalidated due to structural changes
|
|
3
5
|
// This ensures projected capsules are regenerated when the CST format changes
|
|
4
6
|
const CACHE_BUST_VERSION = 22
|
|
@@ -7,7 +9,7 @@ type TSpineOptions = {
|
|
|
7
9
|
spineFilesystemRoot?: string,
|
|
8
10
|
spineContracts: Record<string, any>,
|
|
9
11
|
staticAnalyzer?: any,
|
|
10
|
-
timing?:
|
|
12
|
+
timing?: TimingObserverInterface,
|
|
11
13
|
projectionContext?: {
|
|
12
14
|
capsuleModuleProjectionPackage?: string,
|
|
13
15
|
capsuleModuleProjectionRoot?: string,
|
|
@@ -56,6 +58,9 @@ type TCapsuleSnapshot = {
|
|
|
56
58
|
type TCapsuleMakeInstanceOptions = {
|
|
57
59
|
overrides?: Record<string, any>,
|
|
58
60
|
options?: Record<string, any>,
|
|
61
|
+
/** Nested capsule-name-targeted options from a parent mapping.
|
|
62
|
+
* Merged AFTER options so they take precedence over intermediate defaults. */
|
|
63
|
+
transitiveOverrides?: Record<string, any>,
|
|
59
64
|
runtimeSpineContracts?: Record<string, any>,
|
|
60
65
|
sharedSelf?: Record<string, any>,
|
|
61
66
|
rootCapsule?: {
|
|
@@ -65,7 +70,8 @@ type TCapsuleMakeInstanceOptions = {
|
|
|
65
70
|
},
|
|
66
71
|
parentCapsuleSourceUriLineRefInstanceId?: string,
|
|
67
72
|
sit?: { capsuleInstances: Record<string, { capsuleName: string, capsuleSourceUriLineRef: string, parentCapsuleSourceUriLineRefInstanceId: string }> },
|
|
68
|
-
skipCache?: boolean
|
|
73
|
+
skipCache?: boolean,
|
|
74
|
+
freezePhase?: boolean
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
type TCapsule = {
|
|
@@ -115,6 +121,7 @@ type TSpineContext = {
|
|
|
115
121
|
export const CapsulePropertyTypes = {
|
|
116
122
|
Function: 'Function' as const,
|
|
117
123
|
GetterFunction: 'GetterFunction' as const,
|
|
124
|
+
ConstantGetterFunction: 'ConstantGetterFunction' as const,
|
|
118
125
|
SetterFunction: 'SetterFunction' as const,
|
|
119
126
|
String: 'String' as const,
|
|
120
127
|
Mapping: 'Mapping' as const,
|
|
@@ -125,6 +132,7 @@ export const CapsulePropertyTypes = {
|
|
|
125
132
|
Init: 'Init' as const,
|
|
126
133
|
Dispose: 'Dispose' as const,
|
|
127
134
|
OnFreeze: 'OnFreeze' as const,
|
|
135
|
+
ProxyFunction: 'ProxyFunction' as const,
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
// ##################################################
|
|
@@ -198,8 +206,8 @@ export async function SpineRuntime(options: TSpineRuntimeOptions): Promise<TSpin
|
|
|
198
206
|
|
|
199
207
|
// If the value is a raw capsule instance (has spineContractCapsuleInstances
|
|
200
208
|
// but is NOT a Proxy that handles API access), unwrap it
|
|
201
|
-
// Static
|
|
202
|
-
// Membrane
|
|
209
|
+
// Static sets apiTarget[property.name] = mappedInstance (raw)
|
|
210
|
+
// Membrane sets apiTarget[property.name] = new Proxy(...) which handles API access
|
|
203
211
|
if (value && typeof value === 'object' && value.spineContractCapsuleInstances) {
|
|
204
212
|
// Check if this is a raw capsule instance by seeing if it has .api
|
|
205
213
|
// and the .api doesn't have the same spineContractCapsuleInstances
|
|
@@ -459,7 +467,7 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
|
|
|
459
467
|
snapshot.capsules[capsule.cst.source.capsuleName] = snapshot.capsules[capsuleSourceLineRef]
|
|
460
468
|
}
|
|
461
469
|
|
|
462
|
-
const capsuleInstance = await capsule.makeInstance()
|
|
470
|
+
const capsuleInstance = await capsule.makeInstance({ freezePhase: true })
|
|
463
471
|
|
|
464
472
|
// Run OnFreeze functions so capsules can perform side effects
|
|
465
473
|
// (e.g. file projection) at build/freeze time
|
|
@@ -531,7 +539,7 @@ export async function Spine(options: TSpineOptions): Promise<TSpine> {
|
|
|
531
539
|
const mappedProjectionPath = mappedInstance.mappedPropertyName?.startsWith('/') ? mappedInstance.mappedPropertyName : projectionPath
|
|
532
540
|
// For regular mapped capsules (non-struct delegates), use their own CST as
|
|
533
541
|
// parentCapsuleCst so nested property contract delegates see the correct parent.
|
|
534
|
-
// Property contract delegates (flagged by Static
|
|
542
|
+
// Property contract delegates (flagged by Static) pass through the current
|
|
535
543
|
// parentCapsuleCst so their OnFreeze sees the declaring capsule's CST.
|
|
536
544
|
const mappedCapsule = (!mappedInstance.isPropertyContractDelegate && mappedInstance.capsuleName) ? capsules[mappedInstance.capsuleName] : undefined
|
|
537
545
|
const childParentCst = mappedCapsule?.cst || parentCapsuleCst
|
|
@@ -665,7 +673,9 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
665
673
|
encapsulateOptions,
|
|
666
674
|
cst,
|
|
667
675
|
crt: crts?.[capsuleSourceLineRef],
|
|
668
|
-
makeInstance: async ({ overrides = {}, options = {}, runtimeSpineContracts, sharedSelf, rootCapsule, parentCapsuleSourceUriLineRefInstanceId, sit, skipCache }: TCapsuleMakeInstanceOptions = {}) => {
|
|
676
|
+
makeInstance: async ({ overrides = {}, options = {}, transitiveOverrides, runtimeSpineContracts, sharedSelf, rootCapsule, parentCapsuleSourceUriLineRefInstanceId, sit, skipCache, freezePhase }: TCapsuleMakeInstanceOptions = {}) => {
|
|
677
|
+
|
|
678
|
+
spine.spineOptions.timing?.record(`makeInstance: ${capsuleName || capsuleSourceLineRef}${freezePhase ? ' [freeze]' : ''}${sharedSelf ? ' [extends]' : ''}`)
|
|
669
679
|
|
|
670
680
|
// Create cache key based on parameters
|
|
671
681
|
// When sharedSelf is provided, we must NOT cache because each extending capsule
|
|
@@ -673,15 +683,14 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
673
683
|
// This is critical for the pattern where multiple structs extend the same parent.
|
|
674
684
|
// When skipCache is true (property contract delegates like structs/Capsule),
|
|
675
685
|
// each parent capsule must get its own unique instance.
|
|
676
|
-
const cacheKey = (sharedSelf || skipCache) ? null :
|
|
677
|
-
overrides,
|
|
678
|
-
|
|
679
|
-
hasRuntimeContracts: !!runtimeSpineContracts
|
|
680
|
-
})
|
|
686
|
+
const cacheKey = (sharedSelf || skipCache) ? null : computeCacheKey(
|
|
687
|
+
overrides, options, transitiveOverrides, !!runtimeSpineContracts
|
|
688
|
+
)
|
|
681
689
|
|
|
682
690
|
// Check if we already have a pending or completed instance creation
|
|
683
691
|
// Skip cache when sharedSelf is provided (cacheKey is null)
|
|
684
692
|
if (cacheKey && instanceCache.has(cacheKey)) {
|
|
693
|
+
spine.spineOptions.timing?.record(`makeInstance: CACHE HIT ${capsuleName || capsuleSourceLineRef}`)
|
|
685
694
|
return instanceCache.get(cacheKey)!
|
|
686
695
|
}
|
|
687
696
|
|
|
@@ -723,6 +732,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
723
732
|
const propDefTyped = propDef as Record<string, any>
|
|
724
733
|
const aliasName = propDefTyped.as
|
|
725
734
|
let delegateOptions = propDefTyped.options
|
|
735
|
+
const delegateDepends = propDefTyped.depends
|
|
726
736
|
const contractKey = aliasName || ('#' + propContractUri.substring(1))
|
|
727
737
|
|
|
728
738
|
if (!propertyContractDefinitions[spineContractUri]['#']) {
|
|
@@ -738,6 +748,9 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
738
748
|
value: delegateCapsuleObj || delegateUri,
|
|
739
749
|
propertyContractDelegate: propContractUri,
|
|
740
750
|
as: aliasName,
|
|
751
|
+
// Forward depends from the struct declaration so the delegate
|
|
752
|
+
// options function can access the required parent properties
|
|
753
|
+
depends: delegateDepends,
|
|
741
754
|
// Pass options from the property contract delegate to the mapped capsule
|
|
742
755
|
delegateOptions
|
|
743
756
|
}
|
|
@@ -774,12 +787,19 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
774
787
|
}
|
|
775
788
|
}
|
|
776
789
|
|
|
777
|
-
// Merge in order: overrides
|
|
790
|
+
// Merge in order: overrides, options, then transitive overrides.
|
|
791
|
+
// - Overrides (from run()) are general defaults — options are more specific.
|
|
792
|
+
// - Transitive overrides (nested capsule-name-targeted options from a parent)
|
|
793
|
+
// are the most specific and win over intermediate capsule options.
|
|
778
794
|
mergeByContract(overrides?.[encapsulateOptions.capsuleSourceLineRef], 'Overrides')
|
|
779
795
|
if (encapsulateOptions.capsuleName) {
|
|
780
796
|
mergeByContract(overrides?.[encapsulateOptions.capsuleName], 'Overrides')
|
|
781
797
|
}
|
|
782
798
|
mergeByContract(options, 'Options')
|
|
799
|
+
mergeByContract(transitiveOverrides?.[encapsulateOptions.capsuleSourceLineRef], 'TransitiveOverrides')
|
|
800
|
+
if (encapsulateOptions.capsuleName) {
|
|
801
|
+
mergeByContract(transitiveOverrides?.[encapsulateOptions.capsuleName], 'TransitiveOverrides')
|
|
802
|
+
}
|
|
783
803
|
|
|
784
804
|
// Extract default values from property definitions (Literal/String types)
|
|
785
805
|
// This ensures child capsule's default values are available before parent is instantiated
|
|
@@ -789,7 +809,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
789
809
|
if (propertyContractUri !== '#') continue
|
|
790
810
|
for (const [propertyName, propertyDef] of Object.entries(properties as Record<string, any>)) {
|
|
791
811
|
if (propertyDef.type === CapsulePropertyTypes.Literal ||
|
|
792
|
-
propertyDef.type === CapsulePropertyTypes.String
|
|
812
|
+
propertyDef.type === CapsulePropertyTypes.String ||
|
|
813
|
+
propertyDef.type === CapsulePropertyTypes.Constant) {
|
|
793
814
|
if (propertyDef.value !== undefined) {
|
|
794
815
|
defaultPropertyValues[propertyName] = propertyDef.value
|
|
795
816
|
}
|
|
@@ -846,6 +867,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
846
867
|
capsuleSourceLineRef: absoluteCapsuleSourceLineRef,
|
|
847
868
|
capsuleSourceNameRefHash: cst?.capsuleSourceNameRefHash,
|
|
848
869
|
moduleFilepath: originalAbsoluteModuleFilepath,
|
|
870
|
+
spineFilesystemRoot: spine.spineOptions.spineFilesystemRoot,
|
|
849
871
|
// Root capsule metadata will be populated after extends chain is resolved
|
|
850
872
|
rootCapsule: {
|
|
851
873
|
capsuleName: undefined as string | undefined,
|
|
@@ -925,9 +947,11 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
925
947
|
})
|
|
926
948
|
}
|
|
927
949
|
|
|
950
|
+
spine.spineOptions.timing?.record(`makeInstance: extends → ${extendsCapsule.encapsulateOptions?.capsuleName || '?'}`)
|
|
928
951
|
extendedCapsuleInstance = await extendsCapsule.makeInstance({
|
|
929
952
|
overrides,
|
|
930
953
|
options,
|
|
954
|
+
transitiveOverrides,
|
|
931
955
|
runtimeSpineContracts,
|
|
932
956
|
sharedSelf: self,
|
|
933
957
|
rootCapsule: rootCapsule || {
|
|
@@ -936,9 +960,10 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
936
960
|
moduleFilepath: absoluteModuleFilepath
|
|
937
961
|
},
|
|
938
962
|
parentCapsuleSourceUriLineRefInstanceId: parentCapsuleSourceUriLineRefInstanceId
|
|
939
|
-
?
|
|
940
|
-
:
|
|
941
|
-
sit
|
|
963
|
+
? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + (cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef))
|
|
964
|
+
: sha256(cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef),
|
|
965
|
+
sit,
|
|
966
|
+
freezePhase
|
|
942
967
|
})
|
|
943
968
|
|
|
944
969
|
// Propagate this (child) capsule's encapsulatedApi to all parent spine contract
|
|
@@ -974,8 +999,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
974
999
|
// child: sha256(parentCapsuleSourceUriLineRefInstanceId + ":" + capsuleSourceUriLineRef)
|
|
975
1000
|
const capsuleSourceUriLineRef = cst?.capsuleSourceUriLineRef || encapsulateOptions.capsuleSourceLineRef
|
|
976
1001
|
const capsuleSourceUriLineRefInstanceId = parentCapsuleSourceUriLineRefInstanceId
|
|
977
|
-
?
|
|
978
|
-
:
|
|
1002
|
+
? sha256(parentCapsuleSourceUriLineRefInstanceId + ':' + capsuleSourceUriLineRef)
|
|
1003
|
+
: sha256(capsuleSourceUriLineRef)
|
|
979
1004
|
|
|
980
1005
|
// Register this instance in the sit structure if provided
|
|
981
1006
|
if (sit) {
|
|
@@ -1000,7 +1025,8 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
1000
1025
|
capsuleSourceUriLineRefInstanceId,
|
|
1001
1026
|
capsuleName: encapsulateOptions.capsuleName,
|
|
1002
1027
|
capsuleSourceUriLineRef,
|
|
1003
|
-
sit
|
|
1028
|
+
sit,
|
|
1029
|
+
freezePhase
|
|
1004
1030
|
}
|
|
1005
1031
|
|
|
1006
1032
|
// Set capsule metadata struct on self early so it's available in options() callbacks during mapping
|
|
@@ -1011,6 +1037,14 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
1011
1037
|
}
|
|
1012
1038
|
ownSelf['#@stream44.studio/encapsulate/structs/Capsule'] = capsuleMetadataStruct
|
|
1013
1039
|
|
|
1040
|
+
// Promote metadata values to top-level self properties so struct delegate
|
|
1041
|
+
// proxies (which read from the capsule instance's own self/api) can access them.
|
|
1042
|
+
for (const [metaKey, metaValue] of Object.entries(capsuleMetadataStruct)) {
|
|
1043
|
+
if (metaKey !== 'rootCapsule' && metaValue !== undefined && self[metaKey] === undefined) {
|
|
1044
|
+
self[metaKey] = metaValue
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1014
1048
|
// Use runtime spine contracts if provided, otherwise fall back to encapsulation spine contracts
|
|
1015
1049
|
const activeSpineContracts = runtimeSpineContracts || spine.spineContracts
|
|
1016
1050
|
|
|
@@ -1055,6 +1089,7 @@ async function encapsulate(definition: TCapsuleDefinition, options: TCapsuleOpti
|
|
|
1055
1089
|
await spineContractCapsuleInstance.mapProperty({
|
|
1056
1090
|
overrides,
|
|
1057
1091
|
options,
|
|
1092
|
+
transitiveOverrides,
|
|
1058
1093
|
property: {
|
|
1059
1094
|
name: propertyName,
|
|
1060
1095
|
definition: propertyDefinition,
|
|
@@ -1229,15 +1264,41 @@ function relative(from: string, to: string): string {
|
|
|
1229
1264
|
return result || '.'
|
|
1230
1265
|
}
|
|
1231
1266
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1267
|
+
// Fast cache key computation for makeInstance — avoids JSON.stringify for the common empty case
|
|
1268
|
+
const _emptyObj = {}
|
|
1269
|
+
const _cacheKeyEmpty = '{"overrides":{},"options":{},"hasRuntimeContracts":false}'
|
|
1270
|
+
const _cacheKeyEmptyWithRuntime = '{"overrides":{},"options":{},"hasRuntimeContracts":true}'
|
|
1271
|
+
function _isEmptyObj(obj: any): boolean { for (const _ in obj) return false; return true }
|
|
1272
|
+
const _cacheKeyLastRef = { overrides: _emptyObj as any, options: _emptyObj as any, transitiveOverrides: undefined as any, hasRuntime: false, key: _cacheKeyEmpty }
|
|
1273
|
+
function computeCacheKey(overrides: any, options: any, transitiveOverrides: any, hasRuntimeContracts: boolean): string {
|
|
1274
|
+
// Reference-equality shortcut: if same objects as last call, reuse key
|
|
1275
|
+
if (overrides === _cacheKeyLastRef.overrides && options === _cacheKeyLastRef.options
|
|
1276
|
+
&& transitiveOverrides === _cacheKeyLastRef.transitiveOverrides && hasRuntimeContracts === _cacheKeyLastRef.hasRuntime) {
|
|
1277
|
+
return _cacheKeyLastRef.key
|
|
1239
1278
|
}
|
|
1240
|
-
|
|
1279
|
+
// Fast-path: empty overrides + empty options + no transitive = constant string
|
|
1280
|
+
if (!transitiveOverrides && _isEmptyObj(overrides) && _isEmptyObj(options)) {
|
|
1281
|
+
const key = hasRuntimeContracts ? _cacheKeyEmptyWithRuntime : _cacheKeyEmpty
|
|
1282
|
+
_cacheKeyLastRef.overrides = overrides; _cacheKeyLastRef.options = options
|
|
1283
|
+
_cacheKeyLastRef.transitiveOverrides = transitiveOverrides; _cacheKeyLastRef.hasRuntime = hasRuntimeContracts
|
|
1284
|
+
_cacheKeyLastRef.key = key
|
|
1285
|
+
return key
|
|
1286
|
+
}
|
|
1287
|
+
const key = JSON.stringify({ overrides, options, transitiveOverrides, hasRuntimeContracts })
|
|
1288
|
+
_cacheKeyLastRef.overrides = overrides; _cacheKeyLastRef.options = options
|
|
1289
|
+
_cacheKeyLastRef.transitiveOverrides = transitiveOverrides; _cacheKeyLastRef.hasRuntime = hasRuntimeContracts
|
|
1290
|
+
_cacheKeyLastRef.key = key
|
|
1291
|
+
return key
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const _sha256Cache = new Map<string, string>()
|
|
1295
|
+
const _createHash = require('crypto').createHash
|
|
1296
|
+
function sha256(input: string): string {
|
|
1297
|
+
let result = _sha256Cache.get(input)
|
|
1298
|
+
if (result !== undefined) return result
|
|
1299
|
+
result = _createHash('sha256').update(input).digest('hex') as string
|
|
1300
|
+
_sha256Cache.set(input, result)
|
|
1301
|
+
return result
|
|
1241
1302
|
}
|
|
1242
1303
|
|
|
1243
1304
|
function isObject(item: any): boolean {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CapsulePropertyTypes } from "../../encapsulate"
|
|
2
|
-
import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static
|
|
2
|
+
import { ContractCapsuleInstanceFactory, CapsuleInstanceRegistry } from "./Static"
|
|
3
|
+
import type { TimingObserverInterface } from "../../spine-factories/TimingObserver"
|
|
3
4
|
|
|
4
5
|
type CallerContext = {
|
|
5
6
|
capsuleSourceLineRef: string
|
|
@@ -86,7 +87,8 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
86
87
|
runtimeSpineContracts,
|
|
87
88
|
instanceRegistry,
|
|
88
89
|
extendedCapsuleInstance,
|
|
89
|
-
capsuleInstance
|
|
90
|
+
capsuleInstance,
|
|
91
|
+
timing
|
|
90
92
|
}: {
|
|
91
93
|
spineContractUri: string
|
|
92
94
|
capsule: any
|
|
@@ -109,8 +111,9 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
109
111
|
instanceRegistry?: CapsuleInstanceRegistry
|
|
110
112
|
extendedCapsuleInstance?: any
|
|
111
113
|
capsuleInstance?: any
|
|
114
|
+
timing?: TimingObserverInterface
|
|
112
115
|
}) {
|
|
113
|
-
super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, capsuleInstance })
|
|
116
|
+
super({ spineContractUri, capsule, self, ownSelf, encapsulatedApi, resolve, importCapsule, spineFilesystemRoot, freezeCapsule, instanceRegistry, extendedCapsuleInstance, capsuleInstance, timing })
|
|
114
117
|
this.getEventIndex = getEventIndex
|
|
115
118
|
this.incrementEventIndex = incrementEventIndex
|
|
116
119
|
this.getCurrentCallerContext = getCurrentCallerContext
|
|
@@ -137,7 +140,7 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
137
140
|
return ctx
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
protected async mapMappingProperty({ overrides, options, property }: { overrides: any, options: any, property: any }) {
|
|
143
|
+
protected async mapMappingProperty({ overrides, options, transitiveOverrides, property }: { overrides: any, options: any, transitiveOverrides?: any, property: any }) {
|
|
141
144
|
|
|
142
145
|
const mappedCapsule = await this.resolveMappedCapsule({ property })
|
|
143
146
|
const constants = await this.extractConstants({ mappedCapsule })
|
|
@@ -151,9 +154,23 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
151
154
|
const minimalSelf = this.self[capsuleStructKey]
|
|
152
155
|
? { [capsuleStructKey]: this.self[capsuleStructKey] }
|
|
153
156
|
: {}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
// During freeze phase, skip calling function-based options callbacks — runtime
|
|
158
|
+
// overrides haven't been applied so self.* references may be incomplete.
|
|
159
|
+
// Use a truthy placeholder so the instance registry still creates a fresh instance.
|
|
160
|
+
const isFreezePhase = !!this.capsuleInstance?.freezePhase
|
|
161
|
+
const hasFunctionOptions = typeof optionsFn === 'function'
|
|
162
|
+
const selfArg = { self: property.definition.depends ? this.self : minimalSelf, constants }
|
|
163
|
+
const delegateOpts = property.definition.delegateOptions
|
|
164
|
+
const resolvedDelegateOptions = delegateOpts
|
|
165
|
+
? (typeof delegateOpts === 'function'
|
|
166
|
+
? (isFreezePhase ? {} : await delegateOpts(selfArg))
|
|
167
|
+
: delegateOpts)
|
|
168
|
+
: undefined
|
|
169
|
+
const mappingOptions = resolvedDelegateOptions
|
|
170
|
+
|| (hasFunctionOptions
|
|
171
|
+
? (isFreezePhase
|
|
172
|
+
? {} // truthy placeholder
|
|
173
|
+
: await optionsFn(selfArg))
|
|
157
174
|
: optionsFn)
|
|
158
175
|
|
|
159
176
|
// Check for existing instance in registry - reuse if available (regardless of options)
|
|
@@ -270,13 +287,14 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
270
287
|
}
|
|
271
288
|
}
|
|
272
289
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
290
|
+
// Build transitive overrides for the child: merge any inherited transitive
|
|
291
|
+
// overrides with this mapping's nested capsule-name-targeted options.
|
|
292
|
+
let mappedTransitiveOverrides: Record<string, any> | undefined = transitiveOverrides
|
|
275
293
|
if (nestedCapsuleOptions) {
|
|
276
|
-
|
|
294
|
+
mappedTransitiveOverrides = { ...(mappedTransitiveOverrides || {}) }
|
|
277
295
|
for (const [capsuleNameKey, capsuleOptions] of Object.entries(nestedCapsuleOptions)) {
|
|
278
|
-
|
|
279
|
-
...(
|
|
296
|
+
mappedTransitiveOverrides[capsuleNameKey] = {
|
|
297
|
+
...(mappedTransitiveOverrides[capsuleNameKey] || {}),
|
|
280
298
|
...capsuleOptions
|
|
281
299
|
}
|
|
282
300
|
}
|
|
@@ -285,11 +303,13 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
285
303
|
const mappedCapsuleInstance = await mappedCapsule.makeInstance({
|
|
286
304
|
overrides: mappedOverrides,
|
|
287
305
|
options: ownMappingOptions,
|
|
306
|
+
transitiveOverrides: mappedTransitiveOverrides,
|
|
288
307
|
runtimeSpineContracts: this.runtimeSpineContracts,
|
|
289
308
|
rootCapsule: this.capsuleInstance?.rootCapsule,
|
|
290
309
|
parentCapsuleSourceUriLineRefInstanceId: this.capsuleInstance?.capsuleSourceUriLineRefInstanceId,
|
|
291
310
|
sit: this.capsuleInstance?.sit,
|
|
292
|
-
skipCache: isCapsuleStruct
|
|
311
|
+
skipCache: isCapsuleStruct,
|
|
312
|
+
freezePhase: this.capsuleInstance?.freezePhase || undefined
|
|
293
313
|
})
|
|
294
314
|
|
|
295
315
|
// Register the instance (replaces null pre-registration marker)
|
|
@@ -481,6 +501,51 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
481
501
|
})
|
|
482
502
|
}
|
|
483
503
|
|
|
504
|
+
protected override mapConstantGetterFunctionProperty({ property }: { property: any }) {
|
|
505
|
+
const value = property.definition.value({ constants: this.self })
|
|
506
|
+
|
|
507
|
+
// Store eagerly on self like a Constant
|
|
508
|
+
this.self[property.name] = value
|
|
509
|
+
if (this.ownSelf) {
|
|
510
|
+
this.ownSelf[property.name] = value
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Wrap with membrane event tracking (read-only like Constant)
|
|
514
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
515
|
+
get: () => {
|
|
516
|
+
const currentValue = this.self[property.name]
|
|
517
|
+
|
|
518
|
+
const event: any = {
|
|
519
|
+
event: 'get',
|
|
520
|
+
eventIndex: this.incrementEventIndex(),
|
|
521
|
+
membrane: 'external',
|
|
522
|
+
target: {
|
|
523
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
524
|
+
spineContractCapsuleInstanceId: this.id,
|
|
525
|
+
prop: property.name,
|
|
526
|
+
},
|
|
527
|
+
value: currentValue
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (this.capsuleSourceNameRef) {
|
|
531
|
+
event.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
532
|
+
}
|
|
533
|
+
if (this.capsuleSourceNameRefHash) {
|
|
534
|
+
event.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
this.addCallerContextToEvent(event)
|
|
538
|
+
this.onMembraneEvent?.(event)
|
|
539
|
+
return currentValue
|
|
540
|
+
},
|
|
541
|
+
set: () => {
|
|
542
|
+
throw new Error(`Cannot set ConstantGetterFunction property '${property.name}'`)
|
|
543
|
+
},
|
|
544
|
+
enumerable: true,
|
|
545
|
+
configurable: true
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
|
|
484
549
|
protected mapFunctionProperty({ property }: { property: any }) {
|
|
485
550
|
const selfProxy = this.createSelfProxy()
|
|
486
551
|
const boundFunction = property.definition.value.bind(selfProxy)
|
|
@@ -620,6 +685,86 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
620
685
|
})
|
|
621
686
|
}
|
|
622
687
|
|
|
688
|
+
protected override mapProxyFunctionProperty({ property }: { property: any }) {
|
|
689
|
+
const selfProxy = this.createSelfProxy()
|
|
690
|
+
|
|
691
|
+
const childTargetFn = property.definition.value.target
|
|
692
|
+
const childInvokeFn = property.definition.value.invoke
|
|
693
|
+
|
|
694
|
+
// Inherit missing parts from parent's ProxyFunction (if extending)
|
|
695
|
+
const parentParts = this.self[`__proxyFn_${property.name}`]
|
|
696
|
+
const targetFn = childTargetFn ?? parentParts?.target
|
|
697
|
+
const invokeFn = childInvokeFn ?? parentParts?.invoke
|
|
698
|
+
|
|
699
|
+
if (!targetFn) throw new Error(`ProxyFunction '${property.name}': target() is required`)
|
|
700
|
+
if (!invokeFn) throw new Error(`ProxyFunction '${property.name}': invoke() is required`)
|
|
701
|
+
|
|
702
|
+
// Store parts for potential child override
|
|
703
|
+
this.self[`__proxyFn_${property.name}`] = { target: targetFn, invoke: invokeFn }
|
|
704
|
+
|
|
705
|
+
const boundProxyFn = (...args: any[]) => {
|
|
706
|
+
const transformedArgs = invokeFn.call(selfProxy, ...args)
|
|
707
|
+
const target = targetFn.call(selfProxy)
|
|
708
|
+
if (transformedArgs && typeof transformedArgs.then === 'function') {
|
|
709
|
+
return transformedArgs.then((resolved: any) => target(resolved))
|
|
710
|
+
}
|
|
711
|
+
return target(transformedArgs)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
Object.defineProperty(this.encapsulatedApi, property.name, {
|
|
715
|
+
get: () => {
|
|
716
|
+
return (...args: any[]) => {
|
|
717
|
+
const callEvent: any = {
|
|
718
|
+
event: 'call',
|
|
719
|
+
eventIndex: this.incrementEventIndex(),
|
|
720
|
+
membrane: 'external',
|
|
721
|
+
target: {
|
|
722
|
+
capsuleSourceLineRef: this.encapsulateOptions.capsuleSourceLineRef,
|
|
723
|
+
spineContractCapsuleInstanceId: this.id,
|
|
724
|
+
prop: property.name,
|
|
725
|
+
},
|
|
726
|
+
args
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (this.capsuleSourceNameRef) {
|
|
730
|
+
callEvent.target.capsuleSourceNameRef = this.capsuleSourceNameRef
|
|
731
|
+
}
|
|
732
|
+
if (this.capsuleSourceNameRefHash) {
|
|
733
|
+
callEvent.target.capsuleSourceNameRefHash = this.capsuleSourceNameRefHash
|
|
734
|
+
}
|
|
735
|
+
if (this.capsuleInstance?.capsuleSourceUriLineRefInstanceId) {
|
|
736
|
+
callEvent.target.capsuleSourceUriLineRefInstanceId = this.capsuleInstance.capsuleSourceUriLineRefInstanceId
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
this.addCallerContextToEvent(callEvent)
|
|
740
|
+
this.onMembraneEvent?.(callEvent)
|
|
741
|
+
|
|
742
|
+
const previousCallerContext = this.getCurrentCallerContext()
|
|
743
|
+
this.setCurrentCallerContext(this.buildCallerContext(property.name))
|
|
744
|
+
const result = boundProxyFn(...args)
|
|
745
|
+
this.setCurrentCallerContext(previousCallerContext)
|
|
746
|
+
|
|
747
|
+
const resultEvent: any = {
|
|
748
|
+
event: 'call-result',
|
|
749
|
+
eventIndex: this.incrementEventIndex(),
|
|
750
|
+
membrane: 'external',
|
|
751
|
+
callEventIndex: callEvent.eventIndex,
|
|
752
|
+
target: {
|
|
753
|
+
spineContractCapsuleInstanceId: this.id,
|
|
754
|
+
},
|
|
755
|
+
result
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this.onMembraneEvent?.(resultEvent)
|
|
759
|
+
|
|
760
|
+
return result
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
enumerable: true,
|
|
764
|
+
configurable: true
|
|
765
|
+
})
|
|
766
|
+
}
|
|
767
|
+
|
|
623
768
|
protected mapGetterFunctionProperty({ property }: { property: any }) {
|
|
624
769
|
const getterFn = property.definition.value
|
|
625
770
|
const selfProxy = this.createSelfProxy()
|
|
@@ -749,16 +894,13 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
749
894
|
}
|
|
750
895
|
|
|
751
896
|
// Determine the value source and get the value
|
|
897
|
+
// Virtual dispatch: child overrides take precedence over both
|
|
898
|
+
// self and own encapsulatedApi, matching class-inheritance semantics
|
|
752
899
|
let value: any
|
|
753
900
|
let source: 'self' | 'encapsulatedApi' | 'childApi' | 'extendedApi' | undefined
|
|
754
901
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
source = 'self'
|
|
758
|
-
} else if (prop in factory.encapsulatedApi) {
|
|
759
|
-
value = factory.encapsulatedApi[prop]
|
|
760
|
-
source = 'encapsulatedApi'
|
|
761
|
-
} else if (factory.childEncapsulatedApis) {
|
|
902
|
+
// Check child capsule APIs first for virtual dispatch
|
|
903
|
+
if (factory.childEncapsulatedApis) {
|
|
762
904
|
for (const childApi of factory.childEncapsulatedApis) {
|
|
763
905
|
if (prop in childApi) {
|
|
764
906
|
value = childApi[prop]
|
|
@@ -768,6 +910,16 @@ class MembraneContractCapsuleInstanceFactory extends ContractCapsuleInstanceFact
|
|
|
768
910
|
}
|
|
769
911
|
}
|
|
770
912
|
|
|
913
|
+
if (source === undefined) {
|
|
914
|
+
if (prop in target) {
|
|
915
|
+
value = target[prop]
|
|
916
|
+
source = 'self'
|
|
917
|
+
} else if (prop in factory.encapsulatedApi) {
|
|
918
|
+
value = factory.encapsulatedApi[prop]
|
|
919
|
+
source = 'encapsulatedApi'
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
771
923
|
if (source === undefined && extendedApi && prop in extendedApi) {
|
|
772
924
|
value = extendedApi[prop]
|
|
773
925
|
source = 'extendedApi'
|
|
@@ -914,7 +1066,8 @@ export function CapsuleSpineContract({
|
|
|
914
1066
|
spineFilesystemRoot,
|
|
915
1067
|
resolve,
|
|
916
1068
|
importCapsule,
|
|
917
|
-
npmUriForFilepath
|
|
1069
|
+
npmUriForFilepath,
|
|
1070
|
+
timing
|
|
918
1071
|
}: {
|
|
919
1072
|
onMembraneEvent?: (event: any) => void
|
|
920
1073
|
freezeCapsule?: (capsule: any) => Promise<any>
|
|
@@ -923,6 +1076,7 @@ export function CapsuleSpineContract({
|
|
|
923
1076
|
resolve?: (uri: string, parentFilepath: string) => Promise<string>
|
|
924
1077
|
importCapsule?: (filepath: string) => Promise<any>
|
|
925
1078
|
npmUriForFilepath?: (filepath: string) => Promise<string | null>
|
|
1079
|
+
timing?: TimingObserverInterface
|
|
926
1080
|
} = {}) {
|
|
927
1081
|
|
|
928
1082
|
let eventIndex = 0
|
|
@@ -983,7 +1137,8 @@ export function CapsuleSpineContract({
|
|
|
983
1137
|
runtimeSpineContracts,
|
|
984
1138
|
instanceRegistry,
|
|
985
1139
|
extendedCapsuleInstance,
|
|
986
|
-
capsuleInstance
|
|
1140
|
+
capsuleInstance,
|
|
1141
|
+
timing
|
|
987
1142
|
})
|
|
988
1143
|
},
|
|
989
1144
|
hydrate: ({ capsuleSnapshot }: { capsuleSnapshot: any }): any => {
|