Open johnspackman opened 2 years ago
I've been testing your branch with my code; I've found two issues so far, the second is a blocker
(1) https://github.com/qooxdoo/qooxdoo/blob/new-class-property-system/source/class/qx/Class.js#L804
delete properties[property].refine;
properties[property] =
Object.assign(
{}, allProperties[property] || {}, properties[property]);
// We only get here if `refine : true` was in the configuration.
// That doesn't say whether there was actually a superclass
// property for it to refine. It's not an error to refine a
// non-existing property. Keep track of whether we actually
// refined a property.
refined = property in allProperties;
Because the property's .refine
has been deleted, that's information is lost to reflection; I worked around this by changing the last line in the snippet to:
properties[property].refined = refined = property in allProperties;
(2) Async properties now require a get
and apply
:
https://github.com/qooxdoo/qooxdoo/blob/new-class-property-system/source/class/qx/Class.js#L2643-L2648
This is a proposal for a complete rewrite of the properties mechanism in Qooxdoo classes, it is 100% BC and adds a number of really useful features; a big motivation for this complete rewrite is that the previous system relied on code generation, which was great for IE6 but in modern JS engines this increases memory usage and circumvents JIT compiler technology. Worse of all, the code generation code has grown to be virtually unmaintainable.
The second big motivation is to improve the ease of use, both when defining properties (simpler specification boilerplate) and using them (native properties), and to add better type checking and control over storage and other semantics.
Finally, this is to allow weak references to be built into Qooxdoo in a way that is transparent and can be managed automatically with garbage collection.
New Property Features:
References are included in this PR because property support for References is integral to making References transparent and useful. Reference features are:
Native properties
Add native property getters/setters so that it is possible to use
myObject.someValue
instead ofmyObject.getSomeValue()
/myObject.setSomeValue(…)
.Compiler Changes
This should also work for arrays backed by
qx.data.Array
, so thatmyObject.getMyArray().getItem(0)
becomesmyObject.myArray[0]
but this requires compiler changes to translate all array references into a function, egqx.data.Array.get(myObject.myArray, 0)
which tests for the type ofmyObject.myArray
Eliminate Psuedo Properties
There are lots of places where objects have to define something that looks like a property, but does not list it in the
properties
section. The most common example is usually because the property stores an instance ofqx.data.Array
, and when you callobject.setArrayProperty()
with a new value, the widget only wants to change the contents of the array and does not want to change the actual instance of the array.For example, see qx.ui.form.MModelSelection and the
modelSelection
property.What MModelSelection.js has to do is define a
setModelSelection
,getModelSelection
,resetModelSelection
and an event called"changeModelSelection"
- it must define all four because otherwise the binding will fail. IMHO this is poor because it's not clear from MModelSelection.js whats going on, plus it's reproducing all the existing property code, and it makes it impossible for the framework to add features without declaring that class as broken.There are two ways to solve this in this plan, one is to override the storage, maybe something like this:
or because arrays are a common usage, to bake it into the property mechanism, eg this version:
This would be properly defined, so that
immutable: "replace"
only works on objects which implement some interface egqx.data.IReplacable
, andqx.data.Array
would implement that interface.When the developer tries to set the value of
modelSelection
, the items are copied into the existingmodelSelection
array.You can get the
modelSelection
property and change it directly, and that's part of the reason for this kind of pattern. If you are reacting to changes in themodelSelection
array, you would normally have to listen for "changeModelSelection" to know when the property value changes, and then add/remove listeners for the "change" event for old and new values, and sync the array up to whatever you're listening to it for.It's basically a boat load of extra hard work (and it can get exponential), and so by simply saying "ok this property value will never change, just it's contents" you make life massively easier, without sacrificing compatibility or the dynamics of binding.
Because properties are not always defined in the
properties
section, any code (eg the binding code) cannot rely on reflection to get properties, it has to do it by sniffing.This impacts storage and immutability
Storage
Properties must be able to override the storage mechanism, either by providing your own
get
,set
etc or by specifying that it is held by reference (and to be able to choose the reference implementation).The minimum required would be
get
andset
, all others (egreset
etc) would have default implementationsImmutable Values
Support properties where the value stored is locked; for scalar values, this could mean that the property is read only and attempts to change it are ignored or an exception is raised, for objects like
qx.data.Array
, this would mean that the instance never changes but the contents of the array are changed:Where
immutable
is one of[ null, “replace”, “readOnly” ]
Detecting Mutation
Check whether a property is being changed eg
(Or fire “beforeChangeXxx” and “afterChangeXxx” ??)
First-Class Implementation
Each property should be backed by a first class object, which has
.set(object, value)
,.get(object)
,.check(value)
etc methods. It should be simple to obtain these instances, egmyObject.constructor.getProperty(“someValue”).get()
is the same asmyObject.getSomeValue()
.The qx.Class definition will detect existing pseudo-properties and warn that it is deprecated to use pseudo-properties but wrap the methods with first class property objects in the mean time, so that property reflection is accurate.
A separate project would be to migrate the binding and other classes over to this mechanism.
Redefining
Currently there are only limited options for redefining a property, this will be expanded - specifically, type checks will be able to be further constrained in derived classes
Type Checking
Type checking will become more advanced expression, the same kind of expression that we use for JSDOC - it will include support for parameterised types (eg
"qx.data.Array<MyClass>"
), logical OR “|”, and indicate nullability with a trailing?
Checks will be implemented as a separate, first-class object because this will allow the check code to be used. The checks will follow JSDoc so that, in the future, the compiler could be augmented to copy the JSDoc into type-checking code inserted into the method for parameters and return types.
Deprecate
nullable
Whether a property is nullable or not becomes part of the type checking - initially we will support both, and will provide a tool (similar to
qx es6ify
) to migratenullable
into acheck
. It will only throw an error if there is a conflict, egcheck: "Boolean?", nullable: false
would not be allowedPrivate and Protected Properties
Protected and private properties will be supported, with mangling for private properties.
Fast property definition
Many properties are very similar in that they have a check, an event, an optional
apply
method, and the event name at least follows a strict rule of "changePropertyName" - we know that rule is adhered to because the binding requires it. With nullability included in the type check expression, we can provide an optional "fast" definition of just the type definition:would be the same as:
In this example, note that the event name is assumed and the apply method is auto-detected.
Event Names
Every property will have an event name, unless you specify
event: null
. You can manually provide an event name - although it will be a warning to use an event name which is not in the form "changePropertyName"Asynchronous properties
The tough issue with using asynchronous properties is that you have to await for every stage, and there isn't any clear indication that you need to -
myObject.myArrayProperty[2].myOtherProperty
looks like a normal expression (with native properties), but what ifmyArrayProperty
was defined as asynchronous, and on demand?If
myArrayProperty
was already known, then the getter can return immediately (ie synchronously) but that makes for unpredictable behaviour unless you can guarantee that you fetch the value (withawait myObject.getMyArrayPropertyAsync()
) earlier in the code.Another approach would be to use bindings, except that although bindings do not currently support async properties at all, this is something that could change.
There is a possible work around where a call to an asynchronous function can be made to be synchronous, outlined here in web workers: https://www.smashingmagazine.com/2022/04/partytown-eliminates-website-bloat-third-party-apps/
Init
init
property to accept a function that is called to create the init value; ifinit
is null, then assumenullable:true
(or a nullable type check) unless explicitly assignedAdd the ability to check for an uninitialised properties without triggering an exception because the property is “not yet ready”
Other
apply
to support functions instead of stringsReferences
All Qooxdoo objects stored in properties will be tracked via a Reference class, which can be a HardReference, WeakReference, or OnDemandReference.
WeakReferences will be backed by
WeakRef
which is now in all browsers and nodejs since v14On Demand
OnDemandReferences have a URI that can be used to load an object on demand, and internally use a Hard/Soft/Weak Reference to store the value in the mean time.
This means that there will be some kind of LifecycleManager for different classes as objects come and go
Examples
These are not hard and fast, just some ideas: