@mintjamsinc/ichigojs 0.1.69 → 0.1.70
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 +20 -2
- package/dist/ichigo.cjs +223 -100
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +223 -100
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.min.cjs +1 -1
- package/dist/ichigo.umd.js +223 -100
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VBindings.d.ts +27 -0
- package/package.json +1 -1
package/dist/ichigo.umd.js
CHANGED
|
@@ -8399,6 +8399,31 @@
|
|
|
8399
8399
|
* is replaced, the binding is set to null/undefined, or the bindings are destroyed.
|
|
8400
8400
|
*/
|
|
8401
8401
|
#externalSubscriptions = new Map();
|
|
8402
|
+
/**
|
|
8403
|
+
* Recompute callbacks for computed properties registered on these bindings.
|
|
8404
|
+
* Maps a computed property name to a function that recomputes and caches its value.
|
|
8405
|
+
* Only the bindings instance that owns the computed (typically the root) holds an entry;
|
|
8406
|
+
* child bindings resolve computed reads by delegating to their parent through the proxy.
|
|
8407
|
+
*/
|
|
8408
|
+
#computedRecomputers = new Map();
|
|
8409
|
+
/**
|
|
8410
|
+
* The set of computed properties whose cached value is stale and must be recomputed on next
|
|
8411
|
+
* access (pull-based evaluation). Populated by markComputedDirty when a dependency changes,
|
|
8412
|
+
* and cleared as each computed is resolved.
|
|
8413
|
+
*/
|
|
8414
|
+
#dirtyComputeds = new Set();
|
|
8415
|
+
/**
|
|
8416
|
+
* Guard set used to detect re-entrant (circular) computed resolution. While a computed is
|
|
8417
|
+
* being recomputed its name is held here; a nested read of the same computed returns the
|
|
8418
|
+
* currently cached (stale) value instead of recursing infinitely.
|
|
8419
|
+
*/
|
|
8420
|
+
#resolvingComputeds = new Set();
|
|
8421
|
+
/**
|
|
8422
|
+
* The plain backing object behind the #local proxy. Kept as a direct reference so the cached
|
|
8423
|
+
* value of a computed property can be read without triggering pull-based re-evaluation
|
|
8424
|
+
* (see peekComputed).
|
|
8425
|
+
*/
|
|
8426
|
+
#store = {};
|
|
8402
8427
|
/**
|
|
8403
8428
|
* Creates a new instance of VBindings.
|
|
8404
8429
|
* @param parent The parent bindings, if any.
|
|
@@ -8410,8 +8435,13 @@
|
|
|
8410
8435
|
if (this.#logger?.isDebugEnabled) {
|
|
8411
8436
|
this.#logger.debug(`VBindings created. Parent: ${this.#parent ? 'yes' : 'no'}`);
|
|
8412
8437
|
}
|
|
8413
|
-
this.#local = new Proxy(
|
|
8438
|
+
this.#local = new Proxy(this.#store, {
|
|
8414
8439
|
get: (obj, key) => {
|
|
8440
|
+
// Pull-based evaluation: if this key is a dirty computed property, recompute it now
|
|
8441
|
+
// so the read returns a fresh value (synchronously, even before the update microtask).
|
|
8442
|
+
if (typeof key === 'string') {
|
|
8443
|
+
this.#resolveComputedIfDirty(key);
|
|
8444
|
+
}
|
|
8415
8445
|
if (Reflect.has(obj, key)) {
|
|
8416
8446
|
return Reflect.get(obj, key);
|
|
8417
8447
|
}
|
|
@@ -8669,6 +8699,79 @@
|
|
|
8669
8699
|
registerWritableComputed(key, setter) {
|
|
8670
8700
|
this.#writableComputeds.set(key, setter);
|
|
8671
8701
|
}
|
|
8702
|
+
/**
|
|
8703
|
+
* Registers a computed property for pull-based (lazy) evaluation. The provided recompute
|
|
8704
|
+
* callback is invoked the first time the property is read after it has been marked dirty
|
|
8705
|
+
* (or during the pre-render flush), and is expected to update the cached value (typically
|
|
8706
|
+
* via setSilent).
|
|
8707
|
+
* @param key The computed property name.
|
|
8708
|
+
* @param recompute The callback that recomputes and caches the property's value.
|
|
8709
|
+
*/
|
|
8710
|
+
registerComputed(key, recompute) {
|
|
8711
|
+
this.#computedRecomputers.set(key, recompute);
|
|
8712
|
+
}
|
|
8713
|
+
/**
|
|
8714
|
+
* Marks a computed property as dirty so it will be recomputed on next access.
|
|
8715
|
+
* @param key The computed property name.
|
|
8716
|
+
*/
|
|
8717
|
+
markComputedDirty(key) {
|
|
8718
|
+
this.#dirtyComputeds.add(key);
|
|
8719
|
+
}
|
|
8720
|
+
/**
|
|
8721
|
+
* Reads the currently cached value of a computed property without triggering pull-based
|
|
8722
|
+
* re-evaluation. Used by the recompute routine to obtain the previous value for change
|
|
8723
|
+
* detection. Falls back to the parent bindings when the key is not stored locally.
|
|
8724
|
+
* @param key The computed property name.
|
|
8725
|
+
* @returns The cached value, or undefined if not cached.
|
|
8726
|
+
*/
|
|
8727
|
+
peekComputed(key) {
|
|
8728
|
+
if (Object.prototype.hasOwnProperty.call(this.#store, key)) {
|
|
8729
|
+
return this.#store[key];
|
|
8730
|
+
}
|
|
8731
|
+
return this.#parent?.peekComputed(key);
|
|
8732
|
+
}
|
|
8733
|
+
/**
|
|
8734
|
+
* Forces resolution of every computed property currently marked dirty. Called before the DOM
|
|
8735
|
+
* diff and watcher notification so that the set of changed identifiers is complete.
|
|
8736
|
+
*/
|
|
8737
|
+
flushDirtyComputeds() {
|
|
8738
|
+
for (const key of [...this.#dirtyComputeds]) {
|
|
8739
|
+
this.#resolveComputedIfDirty(key);
|
|
8740
|
+
}
|
|
8741
|
+
}
|
|
8742
|
+
/**
|
|
8743
|
+
* Resolves a computed property if its cached value is dirty (pull-based evaluation), by
|
|
8744
|
+
* invoking its registered recompute callback. Computed→computed chains resolve naturally:
|
|
8745
|
+
* reading another computed inside the getter re-enters this method for that key. Re-entrant
|
|
8746
|
+
* resolution of the same key (a circular dependency) is detected and short-circuited, leaving
|
|
8747
|
+
* the previously cached value in place.
|
|
8748
|
+
* @param key The property name to resolve.
|
|
8749
|
+
*/
|
|
8750
|
+
#resolveComputedIfDirty(key) {
|
|
8751
|
+
const recompute = this.#computedRecomputers.get(key);
|
|
8752
|
+
if (!recompute) {
|
|
8753
|
+
// Not a computed owned by these bindings; a parent (if any) resolves it when the value
|
|
8754
|
+
// is read through the proxy delegation in the get trap.
|
|
8755
|
+
return;
|
|
8756
|
+
}
|
|
8757
|
+
if (!this.#dirtyComputeds.has(key)) {
|
|
8758
|
+
return;
|
|
8759
|
+
}
|
|
8760
|
+
if (this.#resolvingComputeds.has(key)) {
|
|
8761
|
+
this.#logger?.warn(`Circular dependency detected while resolving computed property '${key}'.`);
|
|
8762
|
+
return;
|
|
8763
|
+
}
|
|
8764
|
+
this.#resolvingComputeds.add(key);
|
|
8765
|
+
try {
|
|
8766
|
+
// Clear the dirty flag before recomputing so reads of this same key during recomputation
|
|
8767
|
+
// (other than a true cycle) do not attempt to resolve it again.
|
|
8768
|
+
this.#dirtyComputeds.delete(key);
|
|
8769
|
+
recompute();
|
|
8770
|
+
}
|
|
8771
|
+
finally {
|
|
8772
|
+
this.#resolvingComputeds.delete(key);
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8672
8775
|
/**
|
|
8673
8776
|
* Manually adds an identifier to the set of changed identifiers.
|
|
8674
8777
|
* This is useful for computed properties that need to mark themselves as changed
|
|
@@ -13854,7 +13957,11 @@
|
|
|
13854
13957
|
#initializeBindings() {
|
|
13855
13958
|
// Create bindings with change tracking
|
|
13856
13959
|
this.#bindings = new VBindings({
|
|
13857
|
-
onChange: () => {
|
|
13960
|
+
onChange: (identifier) => {
|
|
13961
|
+
// Pull-based invalidation: mark dependent computed properties dirty synchronously
|
|
13962
|
+
// so a subsequent synchronous read returns a fresh value, then schedule the
|
|
13963
|
+
// batched DOM update for the next microtask.
|
|
13964
|
+
this.#markDirtyComputeds(identifier);
|
|
13858
13965
|
this.#scheduleUpdate();
|
|
13859
13966
|
},
|
|
13860
13967
|
vApplication: this
|
|
@@ -13904,8 +14011,17 @@
|
|
|
13904
14011
|
}
|
|
13905
14012
|
}
|
|
13906
14013
|
}
|
|
13907
|
-
//
|
|
13908
|
-
|
|
14014
|
+
// Register computed properties for pull-based (lazy) evaluation. Each computed is
|
|
14015
|
+
// recomputed on first access after being marked dirty, rather than eagerly in the update
|
|
14016
|
+
// microtask. We mark them all dirty and resolve once here so that initial values are
|
|
14017
|
+
// cached and recorded as changes for the first render.
|
|
14018
|
+
if (this.#options.computed) {
|
|
14019
|
+
for (const key of Object.keys(this.#options.computed)) {
|
|
14020
|
+
this.#bindings.registerComputed(key, () => this.#recomputeOne(key));
|
|
14021
|
+
this.#bindings.markComputedDirty(key);
|
|
14022
|
+
}
|
|
14023
|
+
}
|
|
14024
|
+
this.#flushDirtyComputeds();
|
|
13909
14025
|
}
|
|
13910
14026
|
/**
|
|
13911
14027
|
* Initializes watchers from the watch option.
|
|
@@ -13956,8 +14072,9 @@
|
|
|
13956
14072
|
* Executes an immediate DOM update.
|
|
13957
14073
|
*/
|
|
13958
14074
|
#update() {
|
|
13959
|
-
//
|
|
13960
|
-
|
|
14075
|
+
// Resolve any computed properties still marked dirty (those not already pulled by a
|
|
14076
|
+
// synchronous read) so that the set of changed identifiers is complete before the DOM diff.
|
|
14077
|
+
this.#flushDirtyComputeds();
|
|
13961
14078
|
// Apply computed source path mappings to changes
|
|
13962
14079
|
// This converts paths like "model.elements[0].executionListeners"
|
|
13963
14080
|
// to "selectedElement.executionListeners" when selectedElement points to model.elements[0]
|
|
@@ -14002,110 +14119,116 @@
|
|
|
14002
14119
|
}
|
|
14003
14120
|
}
|
|
14004
14121
|
/**
|
|
14005
|
-
*
|
|
14006
|
-
*
|
|
14122
|
+
* Marks computed properties as dirty (pull-based invalidation) when a dependency changes.
|
|
14123
|
+
* Uses the statically analyzed dependency graph; because computed→computed dependencies are
|
|
14124
|
+
* flattened to their underlying reactive paths during analysis, a single change marks every
|
|
14125
|
+
* transitively dependent computed dirty in one pass. The actual recomputation is deferred until
|
|
14126
|
+
* the value is read (see #recomputeOne).
|
|
14127
|
+
* @param identifier The changed identifier reported by the bindings change tracker.
|
|
14007
14128
|
*/
|
|
14008
|
-
#
|
|
14009
|
-
if (!this.#options.computed) {
|
|
14129
|
+
#markDirtyComputeds(identifier) {
|
|
14130
|
+
if (!this.#options.computed || !this.#bindings) {
|
|
14010
14131
|
return;
|
|
14011
14132
|
}
|
|
14012
|
-
const
|
|
14013
|
-
const processing = new Set();
|
|
14014
|
-
// Gather all changed identifiers, including all parent paths
|
|
14015
|
-
// e.g., for "model.elements[0].messageRef", also add:
|
|
14016
|
-
// "model.elements[0]", "model.elements", "model"
|
|
14017
|
-
const allChanges = new Set();
|
|
14018
|
-
this.#bindings?.changes.forEach(id => {
|
|
14019
|
-
allChanges.add(id);
|
|
14020
|
-
// Add all parent paths by progressively stripping from the end
|
|
14021
|
-
let path = id;
|
|
14022
|
-
while (path.length > 0) {
|
|
14023
|
-
// Find last separator (either '[' or '.')
|
|
14024
|
-
const bracketIdx = path.lastIndexOf('[');
|
|
14025
|
-
const dotIdx = path.lastIndexOf('.');
|
|
14026
|
-
const lastSep = Math.max(bracketIdx, dotIdx);
|
|
14027
|
-
if (lastSep === -1) {
|
|
14028
|
-
break;
|
|
14029
|
-
}
|
|
14030
|
-
path = path.substring(0, lastSep);
|
|
14031
|
-
if (path.length > 0) {
|
|
14032
|
-
allChanges.add(path);
|
|
14033
|
-
}
|
|
14034
|
-
}
|
|
14035
|
-
});
|
|
14036
|
-
// Helper function to recursively compute a property
|
|
14037
|
-
const compute = (key) => {
|
|
14038
|
-
// Skip if already computed in this update cycle
|
|
14039
|
-
if (computed.has(key)) {
|
|
14040
|
-
return;
|
|
14041
|
-
}
|
|
14042
|
-
// Detect circular dependency
|
|
14043
|
-
if (processing.has(key)) {
|
|
14044
|
-
this.#logger.error(`Circular dependency detected for computed property '${key}'`);
|
|
14045
|
-
return;
|
|
14046
|
-
}
|
|
14047
|
-
processing.add(key);
|
|
14048
|
-
// Get the dependencies for this computed property
|
|
14133
|
+
for (const key of Object.keys(this.#computedDependencies)) {
|
|
14049
14134
|
const deps = this.#computedDependencies[key] || [];
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
// dependency chains are resolved and allChanges is up-to-date.
|
|
14053
|
-
for (const dep of deps) {
|
|
14054
|
-
if (this.#options.computed[dep]) {
|
|
14055
|
-
compute(dep);
|
|
14056
|
-
}
|
|
14135
|
+
if (deps.some(dep => this.#dependencyAffectedBy(dep, identifier))) {
|
|
14136
|
+
this.#bindings.markComputedDirty(key);
|
|
14057
14137
|
}
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
14081
|
-
|
|
14082
|
-
|
|
14083
|
-
|
|
14084
|
-
|
|
14085
|
-
|
|
14086
|
-
|
|
14087
|
-
|
|
14088
|
-
|
|
14089
|
-
|
|
14090
|
-
|
|
14091
|
-
|
|
14138
|
+
}
|
|
14139
|
+
}
|
|
14140
|
+
/**
|
|
14141
|
+
* Determines whether a change to `changePath` affects a computed dependency `dep`, taking both
|
|
14142
|
+
* path directions into account:
|
|
14143
|
+
* - an exact match;
|
|
14144
|
+
* - `changePath` is a descendant of `dep` (e.g. dep "cartItems", change "cartItems.0.quantity")
|
|
14145
|
+
* — a nested mutation of the dependency;
|
|
14146
|
+
* - `dep` is a descendant of `changePath` (e.g. dep "user.name", change "user") — the container
|
|
14147
|
+
* holding the dependency was replaced wholesale.
|
|
14148
|
+
* Local and global path aliases (e.g. computed source paths) are also honored via the bindings'
|
|
14149
|
+
* own alias-aware matcher.
|
|
14150
|
+
* @param dep The dependency path declared by a computed property.
|
|
14151
|
+
* @param changePath The identifier reported by the bindings change tracker.
|
|
14152
|
+
*/
|
|
14153
|
+
#dependencyAffectedBy(dep, changePath) {
|
|
14154
|
+
if (changePath === dep) {
|
|
14155
|
+
return true;
|
|
14156
|
+
}
|
|
14157
|
+
if (changePath.startsWith(dep + '.') || changePath.startsWith(dep + '[')) {
|
|
14158
|
+
return true;
|
|
14159
|
+
}
|
|
14160
|
+
if (dep.startsWith(changePath + '.') || dep.startsWith(changePath + '[')) {
|
|
14161
|
+
return true;
|
|
14162
|
+
}
|
|
14163
|
+
// Fall back to alias-aware matching for aliased paths (computed source paths, props, etc.).
|
|
14164
|
+
return this.#bindings.doesChangeMatchIdentifier(changePath, dep);
|
|
14165
|
+
}
|
|
14166
|
+
/**
|
|
14167
|
+
* Forces resolution of all computed properties currently marked dirty so that the set of
|
|
14168
|
+
* changed identifiers is complete before watcher notification and the DOM diff. Computeds that
|
|
14169
|
+
* were already pulled by a synchronous read earlier in the cycle are no longer dirty and are
|
|
14170
|
+
* skipped, so each computed is recomputed at most once per update cycle.
|
|
14171
|
+
*/
|
|
14172
|
+
#flushDirtyComputeds() {
|
|
14173
|
+
this.#bindings?.flushDirtyComputeds();
|
|
14174
|
+
}
|
|
14175
|
+
/**
|
|
14176
|
+
* Recomputes a single computed property and updates its cached value. Registered with the
|
|
14177
|
+
* bindings as the pull-based recompute callback, so it runs lazily the first time the property
|
|
14178
|
+
* is read after being marked dirty (or during the pre-render flush). Computed→computed chains
|
|
14179
|
+
* resolve naturally and order-independently: reading a dependent computed inside the getter
|
|
14180
|
+
* triggers its own lazy resolution through the bindings proxy.
|
|
14181
|
+
*
|
|
14182
|
+
* When the value actually changes, the computed name is recorded as a change so that
|
|
14183
|
+
* dependency-precise DOM updates and watcher notifications still fire, and the source-path
|
|
14184
|
+
* mapping is refreshed so nested changes to the underlying object map back to the computed.
|
|
14185
|
+
* @param key The computed property name.
|
|
14186
|
+
*/
|
|
14187
|
+
#recomputeOne(key) {
|
|
14188
|
+
if (!this.#options.computed || !this.#bindings) {
|
|
14189
|
+
return;
|
|
14190
|
+
}
|
|
14191
|
+
const def = this.#options.computed[key];
|
|
14192
|
+
if (!def) {
|
|
14193
|
+
return;
|
|
14194
|
+
}
|
|
14195
|
+
const computedFn = VApplication.#getComputedGetter(def);
|
|
14196
|
+
try {
|
|
14197
|
+
// Read the previous value without triggering re-resolution, for change detection.
|
|
14198
|
+
const oldValue = this.#bindings.peekComputed(key);
|
|
14199
|
+
const newValue = computedFn.call(this.#bindings.raw);
|
|
14200
|
+
// Check if the value actually changed.
|
|
14201
|
+
let hasChanged = oldValue !== newValue;
|
|
14202
|
+
// For arrays, always treat as changed (VBindings detects length changes via its length
|
|
14203
|
+
// cache). This preserves the precise-update behavior of the previous eager implementation.
|
|
14204
|
+
if (!hasChanged && Array.isArray(newValue)) {
|
|
14205
|
+
hasChanged = true;
|
|
14206
|
+
}
|
|
14207
|
+
// Cache the value silently so the read returns it without re-triggering reactivity.
|
|
14208
|
+
this.#bindings.setSilent(key, newValue);
|
|
14209
|
+
if (hasChanged) {
|
|
14210
|
+
// Mark the computed property as changed so UI and watchers depending on it update.
|
|
14211
|
+
this.#bindings.markChanged(key);
|
|
14212
|
+
// Track source path mapping for computed property values.
|
|
14213
|
+
// This allows changes like "model.elements[0].x" to be mapped to "selectedElement.x".
|
|
14214
|
+
if (typeof newValue === 'object' && newValue !== null) {
|
|
14215
|
+
const sourcePath = ReactiveProxy.getPath(newValue);
|
|
14216
|
+
if (sourcePath) {
|
|
14217
|
+
// Remove old mapping for this computed property
|
|
14218
|
+
for (const [path, name] of this.#computedSourcePaths) {
|
|
14219
|
+
if (name === key) {
|
|
14220
|
+
this.#computedSourcePaths.delete(path);
|
|
14221
|
+
break;
|
|
14092
14222
|
}
|
|
14093
|
-
// Add new mapping
|
|
14094
|
-
this.#computedSourcePaths.set(sourcePath, key);
|
|
14095
14223
|
}
|
|
14224
|
+
// Add new mapping
|
|
14225
|
+
this.#computedSourcePaths.set(sourcePath, key);
|
|
14096
14226
|
}
|
|
14097
14227
|
}
|
|
14098
14228
|
}
|
|
14099
|
-
|
|
14100
|
-
|
|
14101
|
-
}
|
|
14102
|
-
computed.add(key);
|
|
14103
|
-
processing.delete(key);
|
|
14104
|
-
};
|
|
14105
|
-
// Compute all properties; the recursive logic inside compute() handles
|
|
14106
|
-
// dependency ordering and skips properties whose dependencies did not change.
|
|
14107
|
-
for (const key of Object.keys(this.#computedDependencies)) {
|
|
14108
|
-
compute(key);
|
|
14229
|
+
}
|
|
14230
|
+
catch (error) {
|
|
14231
|
+
this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
|
|
14109
14232
|
}
|
|
14110
14233
|
}
|
|
14111
14234
|
/**
|