@mintjamsinc/ichigojs 0.1.1 → 0.1.3

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.
@@ -7044,34 +7044,46 @@ class VBindDirective {
7044
7044
  */
7045
7045
  class ReactiveProxy {
7046
7046
  /**
7047
- * A WeakMap to store the original target for each proxy.
7048
- * This allows us to avoid creating multiple proxies for the same object.
7047
+ * A WeakMap to store the proxy for each target object and path combination.
7048
+ * This prevents creating multiple proxies for the same object accessed from different paths.
7049
7049
  */
7050
- static proxyMap = new WeakMap();
7050
+ static proxyCache = new WeakMap();
7051
7051
  /**
7052
7052
  * Creates a reactive proxy for the given object.
7053
7053
  * The proxy will call the onChange callback whenever a property is modified.
7054
7054
  *
7055
7055
  * @param target The object to make reactive.
7056
- * @param onChange Callback function to call when the object changes. Receives the changed key name.
7056
+ * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
7057
+ * @param path The current path in the object tree (used internally for nested objects).
7057
7058
  * @returns A reactive proxy of the target object.
7058
7059
  */
7059
- static create(target, onChange) {
7060
+ static create(target, onChange, path = '') {
7060
7061
  // If the target is not an object or is null, return it as-is
7061
7062
  if (typeof target !== 'object' || target === null) {
7062
7063
  return target;
7063
7064
  }
7064
- // If this object already has a proxy, return the existing proxy
7065
- if (this.proxyMap.has(target)) {
7066
- return this.proxyMap.get(target);
7065
+ // Check if we already have a proxy for this target with this path
7066
+ let pathMap = this.proxyCache.get(target);
7067
+ if (pathMap) {
7068
+ const existingProxy = pathMap.get(path);
7069
+ if (existingProxy) {
7070
+ return existingProxy;
7071
+ }
7072
+ }
7073
+ else {
7074
+ pathMap = new Map();
7075
+ this.proxyCache.set(target, pathMap);
7067
7076
  }
7068
- // Create the proxy
7077
+ // Create the proxy with path captured in closure
7069
7078
  const proxy = new Proxy(target, {
7070
7079
  get(obj, key) {
7071
7080
  const value = Reflect.get(obj, key);
7072
7081
  // If the value is an object or array, make it reactive too
7073
7082
  if (typeof value === 'object' && value !== null) {
7074
- return ReactiveProxy.create(value, onChange);
7083
+ // Build the nested path
7084
+ const keyStr = String(key);
7085
+ const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7086
+ return ReactiveProxy.create(value, onChange, nestedPath);
7075
7087
  }
7076
7088
  // For arrays, intercept mutation methods
7077
7089
  if (Array.isArray(obj) && typeof value === 'function') {
@@ -7079,7 +7091,7 @@ class ReactiveProxy {
7079
7091
  if (arrayMutationMethods.includes(key)) {
7080
7092
  return function (...args) {
7081
7093
  const result = value.apply(this, args);
7082
- onChange();
7094
+ onChange(path || undefined);
7083
7095
  return result;
7084
7096
  };
7085
7097
  }
@@ -7091,18 +7103,22 @@ class ReactiveProxy {
7091
7103
  const result = Reflect.set(obj, key, value);
7092
7104
  // Only trigger onChange if the value actually changed
7093
7105
  if (oldValue !== value) {
7094
- onChange(key);
7106
+ const keyStr = String(key);
7107
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7108
+ onChange(fullPath);
7095
7109
  }
7096
7110
  return result;
7097
7111
  },
7098
7112
  deleteProperty(obj, key) {
7099
7113
  const result = Reflect.deleteProperty(obj, key);
7100
- onChange(key);
7114
+ const keyStr = String(key);
7115
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7116
+ onChange(fullPath);
7101
7117
  return result;
7102
7118
  }
7103
7119
  });
7104
- // Store the proxy so we can return it if requested again
7105
- this.proxyMap.set(target, proxy);
7120
+ // Cache the proxy for this path
7121
+ pathMap.set(path, proxy);
7106
7122
  return proxy;
7107
7123
  }
7108
7124
  /**
@@ -7112,7 +7128,7 @@ class ReactiveProxy {
7112
7128
  * @returns True if the object is a reactive proxy, false otherwise.
7113
7129
  */
7114
7130
  static isReactive(obj) {
7115
- return this.proxyMap.has(obj);
7131
+ return this.proxyCache.has(obj);
7116
7132
  }
7117
7133
  /**
7118
7134
  * Unwraps a reactive proxy to get the original object.
@@ -7147,10 +7163,18 @@ class VBindings {
7147
7163
  * The change tracker, if any.
7148
7164
  */
7149
7165
  #onChange;
7166
+ /**
7167
+ * The logger instance.
7168
+ */
7169
+ #logger;
7150
7170
  /**
7151
7171
  * The set of changed identifiers.
7152
7172
  */
7153
7173
  #changes = new Set();
7174
+ /**
7175
+ * Cache for array lengths to detect length changes when the same object reference is used.
7176
+ */
7177
+ #lengthCache = new Map();
7154
7178
  /**
7155
7179
  * Creates a new instance of VBindings.
7156
7180
  * @param parent The parent bindings, if any.
@@ -7158,6 +7182,10 @@ class VBindings {
7158
7182
  constructor(args = {}) {
7159
7183
  this.#parent = args.parent;
7160
7184
  this.#onChange = args.onChange;
7185
+ this.#logger = args.vApplication?.logManager.getLogger('VBindings');
7186
+ if (this.#logger?.isDebugEnabled) {
7187
+ this.#logger.debug(`VBindings created. Parent: ${this.#parent ? 'yes' : 'no'}`);
7188
+ }
7161
7189
  this.#local = new Proxy({}, {
7162
7190
  get: (obj, key) => {
7163
7191
  if (Reflect.has(obj, key)) {
@@ -7178,14 +7206,37 @@ class VBindings {
7178
7206
  let newValue = value;
7179
7207
  if (typeof value === 'object' && value !== null) {
7180
7208
  // Wrap objects/arrays with reactive proxy, tracking the root key
7181
- newValue = ReactiveProxy.create(value, () => {
7182
- this.#changes.add(key);
7183
- this.#onChange?.(key);
7184
- });
7209
+ newValue = ReactiveProxy.create(value, (changedPath) => {
7210
+ let path = '';
7211
+ for (const part of changedPath?.split('.') || []) {
7212
+ path = path ? `${path}.${part}` : part;
7213
+ this.#logger?.debug(`Binding changed: ${path}`);
7214
+ this.#changes.add(path);
7215
+ }
7216
+ this.#onChange?.(changedPath);
7217
+ }, key);
7185
7218
  }
7186
7219
  const oldValue = Reflect.get(target, key);
7187
7220
  const result = Reflect.set(target, key, newValue);
7188
- if ((oldValue !== newValue) || (typeof oldValue === 'object' || typeof newValue === 'object')) {
7221
+ // Detect changes
7222
+ let hasChanged = oldValue !== newValue;
7223
+ // Special handling for arrays: check length changes even if same object reference
7224
+ if (Array.isArray(newValue)) {
7225
+ const cachedLength = this.#lengthCache.get(key);
7226
+ const currentLength = newValue.length;
7227
+ if (!hasChanged && cachedLength !== undefined && cachedLength !== currentLength) {
7228
+ hasChanged = true;
7229
+ }
7230
+ this.#lengthCache.set(key, currentLength);
7231
+ }
7232
+ if (hasChanged) {
7233
+ if (this.#logger?.isDebugEnabled) {
7234
+ const oldValueString = typeof oldValue === 'string' ? `"${oldValue}"` : JSON.stringify(oldValue) || 'undefined';
7235
+ const newValueString = typeof newValue === 'string' ? `"${newValue}"` : JSON.stringify(newValue) || 'undefined';
7236
+ const oldValuePreview = oldValueString.length > 100 ? `${oldValueString.substring(0, 100)}...` : oldValueString;
7237
+ const newValuePreview = newValueString.length > 100 ? `${newValueString.substring(0, 100)}...` : newValueString;
7238
+ this.#logger.debug(`Binding set on ${target === obj ? 'local' : 'parent'}: ${key}: ${oldValuePreview} -> ${newValuePreview}`);
7239
+ }
7189
7240
  this.#changes.add(key);
7190
7241
  this.#onChange?.(key);
7191
7242
  }
@@ -7193,6 +7244,7 @@ class VBindings {
7193
7244
  },
7194
7245
  deleteProperty: (obj, key) => {
7195
7246
  const result = Reflect.deleteProperty(obj, key);
7247
+ this.#logger?.debug(`Binding deleted: ${key}`);
7196
7248
  this.#changes.add(key);
7197
7249
  this.#onChange?.(key);
7198
7250
  return result;
@@ -7541,6 +7593,11 @@ class VNode {
7541
7593
  * The data bindings associated with this virtual node, if any.
7542
7594
  */
7543
7595
  #bindings;
7596
+ /**
7597
+ * The initial set of identifiers that this node depends on.
7598
+ * This is optional and may be undefined if there are no dependent identifiers.
7599
+ */
7600
+ #initDependentIdentifiers;
7544
7601
  /**
7545
7602
  * An evaluator for text nodes that contain expressions in {{...}}.
7546
7603
  * This is used to dynamically update the text content based on data bindings.
@@ -7585,6 +7642,7 @@ class VNode {
7585
7642
  this.#nodeName = args.node.nodeName;
7586
7643
  this.#parentVNode = args.parentVNode;
7587
7644
  this.#bindings = args.bindings;
7645
+ this.#initDependentIdentifiers = args.dependentIdentifiers;
7588
7646
  this.#parentVNode?.addChild(this);
7589
7647
  // If the node is a text node, check for expressions and create a text evaluator
7590
7648
  if (this.#nodeType === Node.TEXT_NODE) {
@@ -7736,6 +7794,8 @@ class VNode {
7736
7794
  }
7737
7795
  // Collect identifiers from text evaluator and directives
7738
7796
  const ids = [];
7797
+ // Include initial dependent identifiers, if any
7798
+ ids.push(...this.#initDependentIdentifiers ?? []);
7739
7799
  // If this is a text node with a text evaluator, include its identifiers
7740
7800
  if (this.#textEvaluator) {
7741
7801
  ids.push(...this.#textEvaluator.identifiers);
@@ -7767,6 +7827,29 @@ class VNode {
7767
7827
  this.#preparableIdentifiers = preparableIdentifiers.length === 0 ? [] : [...new Set(preparableIdentifiers)];
7768
7828
  return this.#preparableIdentifiers;
7769
7829
  }
7830
+ /**
7831
+ * The DOM path of this virtual node.
7832
+ * This is a string representation of the path from the root to this node,
7833
+ * using the node names and their indices among siblings with the same name.
7834
+ * For example: "DIV[0]/SPAN[1]/#text[0]"
7835
+ * @return The DOM path as a string.
7836
+ */
7837
+ get domPath() {
7838
+ const path = [];
7839
+ let node = this;
7840
+ while (node) {
7841
+ if (node.parentVNode && node.parentVNode.childVNodes) {
7842
+ const siblings = node.parentVNode.childVNodes.filter(v => v.nodeName === node?.nodeName);
7843
+ const index = siblings.indexOf(node);
7844
+ path.unshift(`${node.nodeName}[${index}]`);
7845
+ }
7846
+ else {
7847
+ path.unshift(node.nodeName);
7848
+ }
7849
+ node = node.parentVNode;
7850
+ }
7851
+ return path.join('/');
7852
+ }
7770
7853
  /**
7771
7854
  * Updates the virtual node and its children based on the current bindings.
7772
7855
  * This method evaluates any expressions in text nodes and applies effectors from directives.
@@ -7863,24 +7946,46 @@ class VNode {
7863
7946
  /**
7864
7947
  * Adds a dependent virtual node that relies on this node's bindings.
7865
7948
  * @param dependent The dependent virtual node to add.
7949
+ * @param dependentIdentifiers The identifiers that the dependent node relies on.
7950
+ * If not provided, the dependent node's own identifiers will be used.
7866
7951
  * @returns A list of closers to unregister the dependency, or undefined if no dependency was added.
7867
7952
  */
7868
- addDependent(dependent) {
7953
+ addDependent(dependent, dependentIdentifiers = undefined) {
7869
7954
  // List of closers to unregister the dependency
7870
7955
  const closers = [];
7871
- // Check if any of the dependent node's identifiers are in this node's identifiers
7872
- let hasIdentifier = dependent.dependentIdentifiers.some(id => this.preparableIdentifiers.includes(id));
7873
- if (!hasIdentifier) {
7874
- hasIdentifier = dependent.dependentIdentifiers.some(id => this.#bindings?.has(id, false) ?? false);
7956
+ // If dependent identifiers are not provided, use the dependent node's own identifiers
7957
+ if (!dependentIdentifiers) {
7958
+ dependentIdentifiers = [...dependent.dependentIdentifiers];
7959
+ }
7960
+ // Prepare alternative identifiers by stripping array indices (e.g., "items[0]" -> "items")
7961
+ const allDeps = new Set();
7962
+ dependentIdentifiers.forEach(id => {
7963
+ allDeps.add(id);
7964
+ const idx = id.indexOf('[');
7965
+ if (idx !== -1) {
7966
+ allDeps.add(id.substring(0, idx));
7967
+ }
7968
+ });
7969
+ // Get this node's identifiers
7970
+ const thisIds = [...this.preparableIdentifiers];
7971
+ if (this.#bindings) {
7972
+ thisIds.push(...this.#bindings?.raw ? Object.keys(this.#bindings.raw) : []);
7875
7973
  }
7876
7974
  // If the dependent node has an identifier in this node's identifiers, add it as a dependency
7877
- if (hasIdentifier) {
7975
+ if ([...allDeps].some(id => thisIds.includes(id))) {
7878
7976
  // If the dependencies list is not initialized, create it
7879
7977
  if (!this.#dependents) {
7880
7978
  this.#dependents = [];
7881
7979
  }
7882
7980
  // Add the dependent node to the list
7883
7981
  this.#dependents.push(dependent);
7982
+ // Remove the matched identifiers from the dependent node's identifiers to avoid duplicate dependencies
7983
+ thisIds.forEach(id => {
7984
+ const idx = dependentIdentifiers.indexOf(id);
7985
+ if (idx !== -1) {
7986
+ dependentIdentifiers.splice(idx, 1);
7987
+ }
7988
+ });
7884
7989
  // Create a closer to unregister the dependency
7885
7990
  closers.push({
7886
7991
  close: () => {
@@ -7893,7 +7998,7 @@ class VNode {
7893
7998
  });
7894
7999
  }
7895
8000
  // Recursively add the dependency to the parent node, if any
7896
- this.#parentVNode?.addDependent(dependent)?.forEach(closer => closers.push(closer));
8001
+ this.#parentVNode?.addDependent(dependent, dependentIdentifiers)?.forEach(closer => closers.push(closer));
7897
8002
  // Return a closer to unregister the dependency
7898
8003
  return closers.length > 0 ? closers : undefined;
7899
8004
  }
@@ -8565,7 +8670,8 @@ class VForDirective {
8565
8670
  node: clone,
8566
8671
  vApplication: this.#vNode.vApplication,
8567
8672
  parentVNode: this.#vNode.parentVNode,
8568
- bindings
8673
+ bindings,
8674
+ dependentIdentifiers: [`${this.#sourceName}[${context.index}]`]
8569
8675
  });
8570
8676
  return vNode;
8571
8677
  }
@@ -8852,10 +8958,13 @@ class VModelDirective {
8852
8958
  }
8853
8959
  // .number modifier: convert to number
8854
8960
  if (this.#modifiers.has('number')) {
8855
- const parsed = Number(result);
8856
- // Only convert if it's a valid number
8857
- if (!isNaN(parsed)) {
8858
- result = parsed;
8961
+ // Skip conversion if the value is empty string
8962
+ if (result !== '') {
8963
+ const parsed = Number(result);
8964
+ // Only convert if it's a valid number
8965
+ if (!isNaN(parsed)) {
8966
+ result = parsed;
8967
+ }
8859
8968
  }
8860
8969
  }
8861
8970
  return result;
@@ -9383,49 +9492,105 @@ var LogLevel;
9383
9492
  })(LogLevel || (LogLevel = {}));
9384
9493
 
9385
9494
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9495
+ /**
9496
+ * A simple logger class for virtual applications.
9497
+ */
9386
9498
  class VLogger {
9499
+ /** The name of the logger. */
9387
9500
  #name;
9501
+ /** The log manager instance. */
9388
9502
  #logManager;
9389
9503
  constructor(name, logManager) {
9390
9504
  this.#name = name;
9391
9505
  this.#logManager = logManager;
9392
9506
  }
9507
+ /**
9508
+ * Indicates whether the debug level is enabled.
9509
+ */
9510
+ get isDebugEnabled() {
9511
+ return [LogLevel.DEBUG].includes(this.#logManager.logLevel);
9512
+ }
9513
+ /**
9514
+ * Indicates whether the info level is enabled.
9515
+ */
9516
+ get isInfoEnabled() {
9517
+ return [LogLevel.DEBUG, LogLevel.INFO].includes(this.#logManager.logLevel);
9518
+ }
9519
+ /**
9520
+ * Indicates whether the warn level is enabled.
9521
+ */
9522
+ get isWarnEnabled() {
9523
+ return [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN].includes(this.#logManager.logLevel);
9524
+ }
9525
+ /**
9526
+ * Logs a debug message.
9527
+ * @param message The message to log.
9528
+ */
9393
9529
  debug(message) {
9394
- if (![LogLevel.DEBUG].includes(this.#logManager.logLevel)) {
9530
+ if (!this.isDebugEnabled) {
9395
9531
  return;
9396
9532
  }
9397
9533
  console.debug(`[${this.#name}] ${LogLevel.DEBUG}: ${message}`);
9398
9534
  }
9535
+ /**
9536
+ * Logs an info message.
9537
+ * @param message The message to log.
9538
+ */
9399
9539
  info(message) {
9400
- if (![LogLevel.DEBUG, LogLevel.INFO].includes(this.#logManager.logLevel)) {
9540
+ if (!this.isInfoEnabled) {
9401
9541
  return;
9402
9542
  }
9403
9543
  console.info(`[${this.#name}] ${LogLevel.INFO}: ${message}`);
9404
9544
  }
9545
+ /**
9546
+ * Logs a warn message.
9547
+ * @param message The message to log.
9548
+ */
9405
9549
  warn(message) {
9406
- if (![LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN].includes(this.#logManager.logLevel)) {
9550
+ if (!this.isWarnEnabled) {
9407
9551
  return;
9408
9552
  }
9409
9553
  console.warn(`[${this.#name}] ${LogLevel.WARN}: ${message}`);
9410
9554
  }
9555
+ /**
9556
+ * Logs an error message.
9557
+ * @param message The message to log.
9558
+ */
9411
9559
  error(message) {
9412
9560
  console.error(`[${this.#name}] ${LogLevel.ERROR}: ${message}`);
9413
9561
  }
9414
9562
  }
9415
9563
 
9416
9564
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9565
+ /**
9566
+ * Manages loggers and their log levels.
9567
+ */
9417
9568
  class VLogManager {
9569
+ /** The current log level. */
9418
9570
  #logLevel;
9571
+ /** A map of logger instances by name. */
9419
9572
  #loggers = new Map();
9420
9573
  constructor(logLevel = LogLevel.INFO) {
9421
9574
  this.#logLevel = logLevel;
9422
9575
  }
9576
+ /**
9577
+ * Sets the log level for all loggers.
9578
+ * @param level The log level to set.
9579
+ */
9423
9580
  set logLevel(level) {
9424
9581
  this.#logLevel = level;
9425
9582
  }
9583
+ /**
9584
+ * Gets the current log level.
9585
+ */
9426
9586
  get logLevel() {
9427
9587
  return this.#logLevel;
9428
9588
  }
9589
+ /**
9590
+ * Gets a logger by name, creating it if it doesn't exist.
9591
+ * @param name The name of the logger.
9592
+ * @returns The logger instance.
9593
+ */
9429
9594
  getLogger(name) {
9430
9595
  if (this.#loggers.has(name)) {
9431
9596
  return this.#loggers.get(name);
@@ -9595,9 +9760,10 @@ class VApplication {
9595
9760
  #initializeBindings() {
9596
9761
  // Create bindings with change tracking
9597
9762
  this.#bindings = new VBindings({
9598
- onChange: (key) => {
9763
+ onChange: (identifier) => {
9599
9764
  this.#scheduleUpdate();
9600
- }
9765
+ },
9766
+ vApplication: this
9601
9767
  });
9602
9768
  // Inject utility methods into bindings
9603
9769
  this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
@@ -9659,6 +9825,15 @@ class VApplication {
9659
9825
  }
9660
9826
  const computed = new Set();
9661
9827
  const processing = new Set();
9828
+ // Gather all changed identifiers, including parent properties for array items
9829
+ const allChanges = new Set();
9830
+ this.#bindings?.changes.forEach(id => {
9831
+ allChanges.add(id);
9832
+ const idx = id.indexOf('[');
9833
+ if (idx !== -1) {
9834
+ allChanges.add(id.substring(0, idx));
9835
+ }
9836
+ });
9662
9837
  // Helper function to recursively compute a property
9663
9838
  const compute = (key) => {
9664
9839
  // Skip if already computed in this update cycle
@@ -9674,7 +9849,7 @@ class VApplication {
9674
9849
  // Get the dependencies for this computed property
9675
9850
  const deps = this.#computedDependencies[key] || [];
9676
9851
  // If none of the dependencies have changed, skip recomputation
9677
- if (!deps.some(dep => this.#bindings?.changes.includes(dep))) {
9852
+ if (!deps.some(dep => allChanges.has(dep))) {
9678
9853
  computed.add(key);
9679
9854
  return;
9680
9855
  }
@@ -9692,7 +9867,7 @@ class VApplication {
9692
9867
  // Track if the computed value actually changed
9693
9868
  if (oldValue !== newValue) {
9694
9869
  this.#bindings?.set(key, newValue);
9695
- this.#logger.debug(`Computed property '${key}' changed: ${oldValue} -> ${newValue}`);
9870
+ allChanges.add(key);
9696
9871
  }
9697
9872
  }
9698
9873
  catch (error) {
@@ -9704,7 +9879,7 @@ class VApplication {
9704
9879
  // Find all computed properties that need to be recomputed
9705
9880
  for (const [key, deps] of Object.entries(this.#computedDependencies)) {
9706
9881
  // Check if any dependency has changed
9707
- if (deps.some(dep => this.#bindings?.changes.includes(dep))) {
9882
+ if (deps.some(dep => allChanges.has(dep))) {
9708
9883
  compute(key);
9709
9884
  }
9710
9885
  }