@mintjamsinc/ichigojs 0.1.67 → 0.1.68

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.
@@ -7975,6 +7975,13 @@ class ReactiveProxy {
7975
7975
  * This allows retrieving the source path of an object for computed property mapping.
7976
7976
  */
7977
7977
  static proxyPaths = new WeakMap();
7978
+ /**
7979
+ * Dispatchers per (target, path). Every target reached while walking a proxy
7980
+ * subtree registers an entry pointing at the same dispatcher as the outermost
7981
+ * proxy of that subtree, so callers can look up the dispatcher from any
7982
+ * intermediate proxy when subscribing.
7983
+ */
7984
+ static dispatchers = new WeakMap();
7978
7985
  /**
7979
7986
  * A Map to store path aliases.
7980
7987
  * Key: alias path (e.g., "editingNestedStep.steps")
@@ -7989,9 +7996,10 @@ class ReactiveProxy {
7989
7996
  * @param target The object to make reactive.
7990
7997
  * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
7991
7998
  * @param path The current path in the object tree (used internally for nested objects).
7999
+ * @param inheritedDispatcher Internal: the dispatcher inherited from an enclosing create() call when wrapping a nested target. External callers must omit this.
7992
8000
  * @returns A reactive proxy of the target object.
7993
8001
  */
7994
- static create(target, onChange, path = '') {
8002
+ static create(target, onChange, path = '', inheritedDispatcher) {
7995
8003
  // If the target is not an object or is null, return it as-is
7996
8004
  if (typeof target !== 'object' || target === null) {
7997
8005
  return target;
@@ -8011,6 +8019,13 @@ class ReactiveProxy {
8011
8019
  if (this.proxyToTarget.has(target)) {
8012
8020
  return target;
8013
8021
  }
8022
+ // Resolve (and register) the dispatcher for this (target, path).
8023
+ // Nested create() calls inherit the dispatcher from the enclosing call so
8024
+ // that a single dispatcher fans out changes at any depth of the subtree.
8025
+ const dispatcher = this.resolveDispatcher(target, path, inheritedDispatcher);
8026
+ if (onChange) {
8027
+ dispatcher.listeners.add(onChange);
8028
+ }
8014
8029
  // Check if we already have a proxy for this target with this path
8015
8030
  let pathMap = this.proxyCache.get(target);
8016
8031
  if (pathMap) {
@@ -8042,7 +8057,7 @@ class ReactiveProxy {
8042
8057
  // Build the nested path
8043
8058
  const keyStr = String(key);
8044
8059
  const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
8045
- return ReactiveProxy.create(value, onChange, nestedPath);
8060
+ return ReactiveProxy.create(value, undefined, nestedPath, dispatcher);
8046
8061
  }
8047
8062
  // If the value is a function, we need to wrap it to ensure that any mutations it performs also trigger onChange
8048
8063
  if (typeof value === 'function') {
@@ -8054,7 +8069,7 @@ class ReactiveProxy {
8054
8069
  }
8055
8070
  return function (...args) {
8056
8071
  const result = value.apply(this === receiver ? obj : this, args);
8057
- onChange(path || undefined);
8072
+ ReactiveProxy.dispatch(dispatcher, path || undefined);
8058
8073
  return result;
8059
8074
  };
8060
8075
  }
@@ -8064,7 +8079,7 @@ class ReactiveProxy {
8064
8079
  return function (...args) {
8065
8080
  const result = value.apply(this === receiver ? obj : this, args);
8066
8081
  if (mapMutationMethods.includes(key)) {
8067
- onChange(path || undefined);
8082
+ ReactiveProxy.dispatch(dispatcher, path || undefined);
8068
8083
  }
8069
8084
  return result;
8070
8085
  };
@@ -8075,7 +8090,7 @@ class ReactiveProxy {
8075
8090
  return function (...args) {
8076
8091
  const result = value.apply(this === receiver ? obj : this, args);
8077
8092
  if (setMutationMethods.includes(key)) {
8078
- onChange(path || undefined);
8093
+ ReactiveProxy.dispatch(dispatcher, path || undefined);
8079
8094
  }
8080
8095
  return result;
8081
8096
  };
@@ -8090,7 +8105,7 @@ class ReactiveProxy {
8090
8105
  if (oldValue !== value) {
8091
8106
  const keyStr = String(key);
8092
8107
  const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
8093
- onChange(fullPath);
8108
+ ReactiveProxy.dispatch(dispatcher, fullPath);
8094
8109
  }
8095
8110
  return result;
8096
8111
  },
@@ -8098,7 +8113,7 @@ class ReactiveProxy {
8098
8113
  const result = Reflect.deleteProperty(obj, key);
8099
8114
  const keyStr = String(key);
8100
8115
  const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
8101
- onChange(fullPath);
8116
+ ReactiveProxy.dispatch(dispatcher, fullPath);
8102
8117
  return result;
8103
8118
  }
8104
8119
  });
@@ -8112,6 +8127,74 @@ class ReactiveProxy {
8112
8127
  }
8113
8128
  return proxy;
8114
8129
  }
8130
+ /**
8131
+ * Looks up the dispatcher associated with (target, path), or installs a new one.
8132
+ * When called for a nested target during proxy walking, the enclosing dispatcher
8133
+ * is reused so a single subtree fans out changes through one notification path.
8134
+ */
8135
+ static resolveDispatcher(target, path, inheritedDispatcher) {
8136
+ let pathMap = this.dispatchers.get(target);
8137
+ if (!pathMap) {
8138
+ pathMap = new Map();
8139
+ this.dispatchers.set(target, pathMap);
8140
+ }
8141
+ const existing = pathMap.get(path);
8142
+ if (existing) {
8143
+ return existing;
8144
+ }
8145
+ const dispatcher = inheritedDispatcher ?? { listeners: new Set() };
8146
+ pathMap.set(path, dispatcher);
8147
+ return dispatcher;
8148
+ }
8149
+ /**
8150
+ * Invokes every listener attached to the dispatcher.
8151
+ * Iterates a snapshot of the listener set so unsubscribing during dispatch is safe.
8152
+ */
8153
+ static dispatch(dispatcher, changedPath) {
8154
+ if (dispatcher.listeners.size === 0) {
8155
+ return;
8156
+ }
8157
+ const snapshot = Array.from(dispatcher.listeners);
8158
+ for (const listener of snapshot) {
8159
+ listener(changedPath);
8160
+ }
8161
+ }
8162
+ /**
8163
+ * Subscribes a listener to changes inside the subtree of an existing reactive proxy.
8164
+ *
8165
+ * The listener is scoped by the proxy's source path: only changes at or below that
8166
+ * path are delivered, which lets a child component receive notifications when the
8167
+ * nested contents of a prop change even though the prop reference itself is unchanged.
8168
+ *
8169
+ * @param proxyOrTarget A proxy returned from create(), or the underlying target object.
8170
+ * @param listener Called with the full source path of every relevant change.
8171
+ * @returns A function that removes the subscription.
8172
+ */
8173
+ static subscribe(proxyOrTarget, listener) {
8174
+ if (typeof proxyOrTarget !== 'object' || proxyOrTarget === null) {
8175
+ return () => { };
8176
+ }
8177
+ const target = (this.proxyToTarget.get(proxyOrTarget) ?? proxyOrTarget);
8178
+ const scopePath = this.proxyPaths.get(proxyOrTarget) ?? '';
8179
+ const pathMap = this.dispatchers.get(target);
8180
+ const dispatcher = pathMap?.get(scopePath);
8181
+ if (!dispatcher) {
8182
+ return () => { };
8183
+ }
8184
+ const wrapper = scopePath
8185
+ ? (changedPath) => {
8186
+ if (changedPath === scopePath ||
8187
+ (typeof changedPath === 'string' && (changedPath.startsWith(scopePath + '.') ||
8188
+ changedPath.startsWith(scopePath + '[')))) {
8189
+ listener(changedPath);
8190
+ }
8191
+ }
8192
+ : listener;
8193
+ dispatcher.listeners.add(wrapper);
8194
+ return () => {
8195
+ dispatcher.listeners.delete(wrapper);
8196
+ };
8197
+ }
8115
8198
  /**
8116
8199
  * Checks if the given object is a reactive proxy.
8117
8200
  *
@@ -8302,6 +8385,14 @@ class VBindings {
8302
8385
  * is updated by the recompute cycle through `setSilent`, which bypasses this routing.
8303
8386
  */
8304
8387
  #writableComputeds = new Map();
8388
+ /**
8389
+ * Unsubscribe handles for external reactive proxies received as binding values
8390
+ * (typically component props). Each entry keeps the child bindings notified when
8391
+ * nested properties of a shared object change even though the prop reference itself
8392
+ * stays the same. The previous subscription is released whenever the binding value
8393
+ * is replaced, the binding is set to null/undefined, or the bindings are destroyed.
8394
+ */
8395
+ #externalSubscriptions = new Map();
8305
8396
  /**
8306
8397
  * Creates a new instance of VBindings.
8307
8398
  * @param parent The parent bindings, if any.
@@ -8339,6 +8430,7 @@ class VBindings {
8339
8430
  }
8340
8431
  }
8341
8432
  let newValue = value;
8433
+ let receivedExternalProxy = false;
8342
8434
  if (typeof value === 'object' && value !== null) {
8343
8435
  // Check if the value already has a path (it's an existing reactive proxy reference)
8344
8436
  const existingPath = ReactiveProxy.getPath(value);
@@ -8348,6 +8440,7 @@ class VBindings {
8348
8440
  this.#logger?.debug(`Path alias registered: ${key} -> ${existingPath}`);
8349
8441
  // Keep the existing proxy as-is to preserve reactivity chain
8350
8442
  newValue = value;
8443
+ receivedExternalProxy = true;
8351
8444
  }
8352
8445
  else {
8353
8446
  // Before wrapping, check if any properties are existing ReactiveProxies
@@ -8391,6 +8484,34 @@ class VBindings {
8391
8484
  }
8392
8485
  const oldValue = Reflect.get(target, key);
8393
8486
  const result = Reflect.set(target, key, newValue);
8487
+ // Manage external subscription to a reactive proxy received as the value.
8488
+ // When the reference changes, drop the previous subscription. When a new
8489
+ // reactive proxy is installed, subscribe so nested changes propagate to
8490
+ // this bindings instance even though the proxy reference itself is stable.
8491
+ if (oldValue !== newValue) {
8492
+ const prevUnsubscribe = this.#externalSubscriptions.get(key);
8493
+ if (prevUnsubscribe) {
8494
+ prevUnsubscribe();
8495
+ this.#externalSubscriptions.delete(key);
8496
+ }
8497
+ }
8498
+ if (receivedExternalProxy && !this.#externalSubscriptions.has(key)) {
8499
+ const unsubscribe = ReactiveProxy.subscribe(newValue, (changedPath) => {
8500
+ if (!changedPath) {
8501
+ return;
8502
+ }
8503
+ let path = '';
8504
+ for (const part of changedPath.split('.')) {
8505
+ path = path ? `${path}.${part}` : part;
8506
+ this.#logger?.debug(`Binding changed (external): ${path}`);
8507
+ this.#changes.add(path);
8508
+ }
8509
+ if (!this.#suppressOnChange) {
8510
+ this.#onChange?.(changedPath);
8511
+ }
8512
+ });
8513
+ this.#externalSubscriptions.set(key, unsubscribe);
8514
+ }
8394
8515
  // Detect changes
8395
8516
  let hasChanged = oldValue !== newValue;
8396
8517
  // Special handling for arrays: check length changes even if same object reference
@@ -8419,6 +8540,11 @@ class VBindings {
8419
8540
  },
8420
8541
  deleteProperty: (obj, key) => {
8421
8542
  const result = Reflect.deleteProperty(obj, key);
8543
+ const prevUnsubscribe = this.#externalSubscriptions.get(key);
8544
+ if (prevUnsubscribe) {
8545
+ prevUnsubscribe();
8546
+ this.#externalSubscriptions.delete(key);
8547
+ }
8422
8548
  this.#logger?.debug(`Binding deleted: ${key}`);
8423
8549
  this.#changes.add(key);
8424
8550
  this.#onChange?.(key);
@@ -8500,6 +8626,18 @@ class VBindings {
8500
8626
  remove(key) {
8501
8627
  delete this.#local[key];
8502
8628
  }
8629
+ /**
8630
+ * Releases all external proxy subscriptions held by these bindings.
8631
+ * Should be called when the owning application is unmounted so the parent
8632
+ * application's reactive objects do not keep references to listener closures
8633
+ * (and through them, this bindings instance) alive.
8634
+ */
8635
+ destroy() {
8636
+ for (const unsubscribe of this.#externalSubscriptions.values()) {
8637
+ unsubscribe();
8638
+ }
8639
+ this.#externalSubscriptions.clear();
8640
+ }
8503
8641
  /**
8504
8642
  * Sets a binding value without triggering onChange callback.
8505
8643
  * This is useful for internal updates that shouldn't trigger reactivity.
@@ -13574,6 +13712,7 @@ class VApplication {
13574
13712
  this.#vNode.destroy();
13575
13713
  this.#vNode = undefined;
13576
13714
  }
13715
+ this.#bindings?.destroy();
13577
13716
  this.#logger.info('Application unmounted.');
13578
13717
  }
13579
13718
  /**