@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.
@@ -7050,34 +7050,46 @@
7050
7050
  */
7051
7051
  class ReactiveProxy {
7052
7052
  /**
7053
- * A WeakMap to store the original target for each proxy.
7054
- * This allows us to avoid creating multiple proxies for the same object.
7053
+ * A WeakMap to store the proxy for each target object and path combination.
7054
+ * This prevents creating multiple proxies for the same object accessed from different paths.
7055
7055
  */
7056
- static proxyMap = new WeakMap();
7056
+ static proxyCache = new WeakMap();
7057
7057
  /**
7058
7058
  * Creates a reactive proxy for the given object.
7059
7059
  * The proxy will call the onChange callback whenever a property is modified.
7060
7060
  *
7061
7061
  * @param target The object to make reactive.
7062
- * @param onChange Callback function to call when the object changes. Receives the changed key name.
7062
+ * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
7063
+ * @param path The current path in the object tree (used internally for nested objects).
7063
7064
  * @returns A reactive proxy of the target object.
7064
7065
  */
7065
- static create(target, onChange) {
7066
+ static create(target, onChange, path = '') {
7066
7067
  // If the target is not an object or is null, return it as-is
7067
7068
  if (typeof target !== 'object' || target === null) {
7068
7069
  return target;
7069
7070
  }
7070
- // If this object already has a proxy, return the existing proxy
7071
- if (this.proxyMap.has(target)) {
7072
- return this.proxyMap.get(target);
7071
+ // Check if we already have a proxy for this target with this path
7072
+ let pathMap = this.proxyCache.get(target);
7073
+ if (pathMap) {
7074
+ const existingProxy = pathMap.get(path);
7075
+ if (existingProxy) {
7076
+ return existingProxy;
7077
+ }
7078
+ }
7079
+ else {
7080
+ pathMap = new Map();
7081
+ this.proxyCache.set(target, pathMap);
7073
7082
  }
7074
- // Create the proxy
7083
+ // Create the proxy with path captured in closure
7075
7084
  const proxy = new Proxy(target, {
7076
7085
  get(obj, key) {
7077
7086
  const value = Reflect.get(obj, key);
7078
7087
  // If the value is an object or array, make it reactive too
7079
7088
  if (typeof value === 'object' && value !== null) {
7080
- return ReactiveProxy.create(value, onChange);
7089
+ // Build the nested path
7090
+ const keyStr = String(key);
7091
+ const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7092
+ return ReactiveProxy.create(value, onChange, nestedPath);
7081
7093
  }
7082
7094
  // For arrays, intercept mutation methods
7083
7095
  if (Array.isArray(obj) && typeof value === 'function') {
@@ -7085,7 +7097,7 @@
7085
7097
  if (arrayMutationMethods.includes(key)) {
7086
7098
  return function (...args) {
7087
7099
  const result = value.apply(this, args);
7088
- onChange();
7100
+ onChange(path || undefined);
7089
7101
  return result;
7090
7102
  };
7091
7103
  }
@@ -7097,18 +7109,22 @@
7097
7109
  const result = Reflect.set(obj, key, value);
7098
7110
  // Only trigger onChange if the value actually changed
7099
7111
  if (oldValue !== value) {
7100
- onChange(key);
7112
+ const keyStr = String(key);
7113
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7114
+ onChange(fullPath);
7101
7115
  }
7102
7116
  return result;
7103
7117
  },
7104
7118
  deleteProperty(obj, key) {
7105
7119
  const result = Reflect.deleteProperty(obj, key);
7106
- onChange(key);
7120
+ const keyStr = String(key);
7121
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7122
+ onChange(fullPath);
7107
7123
  return result;
7108
7124
  }
7109
7125
  });
7110
- // Store the proxy so we can return it if requested again
7111
- this.proxyMap.set(target, proxy);
7126
+ // Cache the proxy for this path
7127
+ pathMap.set(path, proxy);
7112
7128
  return proxy;
7113
7129
  }
7114
7130
  /**
@@ -7118,7 +7134,7 @@
7118
7134
  * @returns True if the object is a reactive proxy, false otherwise.
7119
7135
  */
7120
7136
  static isReactive(obj) {
7121
- return this.proxyMap.has(obj);
7137
+ return this.proxyCache.has(obj);
7122
7138
  }
7123
7139
  /**
7124
7140
  * Unwraps a reactive proxy to get the original object.
@@ -7153,10 +7169,18 @@
7153
7169
  * The change tracker, if any.
7154
7170
  */
7155
7171
  #onChange;
7172
+ /**
7173
+ * The logger instance.
7174
+ */
7175
+ #logger;
7156
7176
  /**
7157
7177
  * The set of changed identifiers.
7158
7178
  */
7159
7179
  #changes = new Set();
7180
+ /**
7181
+ * Cache for array lengths to detect length changes when the same object reference is used.
7182
+ */
7183
+ #lengthCache = new Map();
7160
7184
  /**
7161
7185
  * Creates a new instance of VBindings.
7162
7186
  * @param parent The parent bindings, if any.
@@ -7164,6 +7188,10 @@
7164
7188
  constructor(args = {}) {
7165
7189
  this.#parent = args.parent;
7166
7190
  this.#onChange = args.onChange;
7191
+ this.#logger = args.vApplication?.logManager.getLogger('VBindings');
7192
+ if (this.#logger?.isDebugEnabled) {
7193
+ this.#logger.debug(`VBindings created. Parent: ${this.#parent ? 'yes' : 'no'}`);
7194
+ }
7167
7195
  this.#local = new Proxy({}, {
7168
7196
  get: (obj, key) => {
7169
7197
  if (Reflect.has(obj, key)) {
@@ -7184,14 +7212,37 @@
7184
7212
  let newValue = value;
7185
7213
  if (typeof value === 'object' && value !== null) {
7186
7214
  // Wrap objects/arrays with reactive proxy, tracking the root key
7187
- newValue = ReactiveProxy.create(value, () => {
7188
- this.#changes.add(key);
7189
- this.#onChange?.(key);
7190
- });
7215
+ newValue = ReactiveProxy.create(value, (changedPath) => {
7216
+ let path = '';
7217
+ for (const part of changedPath?.split('.') || []) {
7218
+ path = path ? `${path}.${part}` : part;
7219
+ this.#logger?.debug(`Binding changed: ${path}`);
7220
+ this.#changes.add(path);
7221
+ }
7222
+ this.#onChange?.(changedPath);
7223
+ }, key);
7191
7224
  }
7192
7225
  const oldValue = Reflect.get(target, key);
7193
7226
  const result = Reflect.set(target, key, newValue);
7194
- if ((oldValue !== newValue) || (typeof oldValue === 'object' || typeof newValue === 'object')) {
7227
+ // Detect changes
7228
+ let hasChanged = oldValue !== newValue;
7229
+ // Special handling for arrays: check length changes even if same object reference
7230
+ if (Array.isArray(newValue)) {
7231
+ const cachedLength = this.#lengthCache.get(key);
7232
+ const currentLength = newValue.length;
7233
+ if (!hasChanged && cachedLength !== undefined && cachedLength !== currentLength) {
7234
+ hasChanged = true;
7235
+ }
7236
+ this.#lengthCache.set(key, currentLength);
7237
+ }
7238
+ if (hasChanged) {
7239
+ if (this.#logger?.isDebugEnabled) {
7240
+ const oldValueString = typeof oldValue === 'string' ? `"${oldValue}"` : JSON.stringify(oldValue) || 'undefined';
7241
+ const newValueString = typeof newValue === 'string' ? `"${newValue}"` : JSON.stringify(newValue) || 'undefined';
7242
+ const oldValuePreview = oldValueString.length > 100 ? `${oldValueString.substring(0, 100)}...` : oldValueString;
7243
+ const newValuePreview = newValueString.length > 100 ? `${newValueString.substring(0, 100)}...` : newValueString;
7244
+ this.#logger.debug(`Binding set on ${target === obj ? 'local' : 'parent'}: ${key}: ${oldValuePreview} -> ${newValuePreview}`);
7245
+ }
7195
7246
  this.#changes.add(key);
7196
7247
  this.#onChange?.(key);
7197
7248
  }
@@ -7199,6 +7250,7 @@
7199
7250
  },
7200
7251
  deleteProperty: (obj, key) => {
7201
7252
  const result = Reflect.deleteProperty(obj, key);
7253
+ this.#logger?.debug(`Binding deleted: ${key}`);
7202
7254
  this.#changes.add(key);
7203
7255
  this.#onChange?.(key);
7204
7256
  return result;
@@ -7547,6 +7599,11 @@
7547
7599
  * The data bindings associated with this virtual node, if any.
7548
7600
  */
7549
7601
  #bindings;
7602
+ /**
7603
+ * The initial set of identifiers that this node depends on.
7604
+ * This is optional and may be undefined if there are no dependent identifiers.
7605
+ */
7606
+ #initDependentIdentifiers;
7550
7607
  /**
7551
7608
  * An evaluator for text nodes that contain expressions in {{...}}.
7552
7609
  * This is used to dynamically update the text content based on data bindings.
@@ -7591,6 +7648,7 @@
7591
7648
  this.#nodeName = args.node.nodeName;
7592
7649
  this.#parentVNode = args.parentVNode;
7593
7650
  this.#bindings = args.bindings;
7651
+ this.#initDependentIdentifiers = args.dependentIdentifiers;
7594
7652
  this.#parentVNode?.addChild(this);
7595
7653
  // If the node is a text node, check for expressions and create a text evaluator
7596
7654
  if (this.#nodeType === Node.TEXT_NODE) {
@@ -7742,6 +7800,8 @@
7742
7800
  }
7743
7801
  // Collect identifiers from text evaluator and directives
7744
7802
  const ids = [];
7803
+ // Include initial dependent identifiers, if any
7804
+ ids.push(...this.#initDependentIdentifiers ?? []);
7745
7805
  // If this is a text node with a text evaluator, include its identifiers
7746
7806
  if (this.#textEvaluator) {
7747
7807
  ids.push(...this.#textEvaluator.identifiers);
@@ -7773,6 +7833,29 @@
7773
7833
  this.#preparableIdentifiers = preparableIdentifiers.length === 0 ? [] : [...new Set(preparableIdentifiers)];
7774
7834
  return this.#preparableIdentifiers;
7775
7835
  }
7836
+ /**
7837
+ * The DOM path of this virtual node.
7838
+ * This is a string representation of the path from the root to this node,
7839
+ * using the node names and their indices among siblings with the same name.
7840
+ * For example: "DIV[0]/SPAN[1]/#text[0]"
7841
+ * @return The DOM path as a string.
7842
+ */
7843
+ get domPath() {
7844
+ const path = [];
7845
+ let node = this;
7846
+ while (node) {
7847
+ if (node.parentVNode && node.parentVNode.childVNodes) {
7848
+ const siblings = node.parentVNode.childVNodes.filter(v => v.nodeName === node?.nodeName);
7849
+ const index = siblings.indexOf(node);
7850
+ path.unshift(`${node.nodeName}[${index}]`);
7851
+ }
7852
+ else {
7853
+ path.unshift(node.nodeName);
7854
+ }
7855
+ node = node.parentVNode;
7856
+ }
7857
+ return path.join('/');
7858
+ }
7776
7859
  /**
7777
7860
  * Updates the virtual node and its children based on the current bindings.
7778
7861
  * This method evaluates any expressions in text nodes and applies effectors from directives.
@@ -7869,24 +7952,46 @@
7869
7952
  /**
7870
7953
  * Adds a dependent virtual node that relies on this node's bindings.
7871
7954
  * @param dependent The dependent virtual node to add.
7955
+ * @param dependentIdentifiers The identifiers that the dependent node relies on.
7956
+ * If not provided, the dependent node's own identifiers will be used.
7872
7957
  * @returns A list of closers to unregister the dependency, or undefined if no dependency was added.
7873
7958
  */
7874
- addDependent(dependent) {
7959
+ addDependent(dependent, dependentIdentifiers = undefined) {
7875
7960
  // List of closers to unregister the dependency
7876
7961
  const closers = [];
7877
- // Check if any of the dependent node's identifiers are in this node's identifiers
7878
- let hasIdentifier = dependent.dependentIdentifiers.some(id => this.preparableIdentifiers.includes(id));
7879
- if (!hasIdentifier) {
7880
- hasIdentifier = dependent.dependentIdentifiers.some(id => this.#bindings?.has(id, false) ?? false);
7962
+ // If dependent identifiers are not provided, use the dependent node's own identifiers
7963
+ if (!dependentIdentifiers) {
7964
+ dependentIdentifiers = [...dependent.dependentIdentifiers];
7965
+ }
7966
+ // Prepare alternative identifiers by stripping array indices (e.g., "items[0]" -> "items")
7967
+ const allDeps = new Set();
7968
+ dependentIdentifiers.forEach(id => {
7969
+ allDeps.add(id);
7970
+ const idx = id.indexOf('[');
7971
+ if (idx !== -1) {
7972
+ allDeps.add(id.substring(0, idx));
7973
+ }
7974
+ });
7975
+ // Get this node's identifiers
7976
+ const thisIds = [...this.preparableIdentifiers];
7977
+ if (this.#bindings) {
7978
+ thisIds.push(...this.#bindings?.raw ? Object.keys(this.#bindings.raw) : []);
7881
7979
  }
7882
7980
  // If the dependent node has an identifier in this node's identifiers, add it as a dependency
7883
- if (hasIdentifier) {
7981
+ if ([...allDeps].some(id => thisIds.includes(id))) {
7884
7982
  // If the dependencies list is not initialized, create it
7885
7983
  if (!this.#dependents) {
7886
7984
  this.#dependents = [];
7887
7985
  }
7888
7986
  // Add the dependent node to the list
7889
7987
  this.#dependents.push(dependent);
7988
+ // Remove the matched identifiers from the dependent node's identifiers to avoid duplicate dependencies
7989
+ thisIds.forEach(id => {
7990
+ const idx = dependentIdentifiers.indexOf(id);
7991
+ if (idx !== -1) {
7992
+ dependentIdentifiers.splice(idx, 1);
7993
+ }
7994
+ });
7890
7995
  // Create a closer to unregister the dependency
7891
7996
  closers.push({
7892
7997
  close: () => {
@@ -7899,7 +8004,7 @@
7899
8004
  });
7900
8005
  }
7901
8006
  // Recursively add the dependency to the parent node, if any
7902
- this.#parentVNode?.addDependent(dependent)?.forEach(closer => closers.push(closer));
8007
+ this.#parentVNode?.addDependent(dependent, dependentIdentifiers)?.forEach(closer => closers.push(closer));
7903
8008
  // Return a closer to unregister the dependency
7904
8009
  return closers.length > 0 ? closers : undefined;
7905
8010
  }
@@ -8571,7 +8676,8 @@
8571
8676
  node: clone,
8572
8677
  vApplication: this.#vNode.vApplication,
8573
8678
  parentVNode: this.#vNode.parentVNode,
8574
- bindings
8679
+ bindings,
8680
+ dependentIdentifiers: [`${this.#sourceName}[${context.index}]`]
8575
8681
  });
8576
8682
  return vNode;
8577
8683
  }
@@ -8858,10 +8964,13 @@
8858
8964
  }
8859
8965
  // .number modifier: convert to number
8860
8966
  if (this.#modifiers.has('number')) {
8861
- const parsed = Number(result);
8862
- // Only convert if it's a valid number
8863
- if (!isNaN(parsed)) {
8864
- result = parsed;
8967
+ // Skip conversion if the value is empty string
8968
+ if (result !== '') {
8969
+ const parsed = Number(result);
8970
+ // Only convert if it's a valid number
8971
+ if (!isNaN(parsed)) {
8972
+ result = parsed;
8973
+ }
8865
8974
  }
8866
8975
  }
8867
8976
  return result;
@@ -9389,49 +9498,105 @@
9389
9498
  })(LogLevel || (LogLevel = {}));
9390
9499
 
9391
9500
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9501
+ /**
9502
+ * A simple logger class for virtual applications.
9503
+ */
9392
9504
  class VLogger {
9505
+ /** The name of the logger. */
9393
9506
  #name;
9507
+ /** The log manager instance. */
9394
9508
  #logManager;
9395
9509
  constructor(name, logManager) {
9396
9510
  this.#name = name;
9397
9511
  this.#logManager = logManager;
9398
9512
  }
9513
+ /**
9514
+ * Indicates whether the debug level is enabled.
9515
+ */
9516
+ get isDebugEnabled() {
9517
+ return [LogLevel.DEBUG].includes(this.#logManager.logLevel);
9518
+ }
9519
+ /**
9520
+ * Indicates whether the info level is enabled.
9521
+ */
9522
+ get isInfoEnabled() {
9523
+ return [LogLevel.DEBUG, LogLevel.INFO].includes(this.#logManager.logLevel);
9524
+ }
9525
+ /**
9526
+ * Indicates whether the warn level is enabled.
9527
+ */
9528
+ get isWarnEnabled() {
9529
+ return [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN].includes(this.#logManager.logLevel);
9530
+ }
9531
+ /**
9532
+ * Logs a debug message.
9533
+ * @param message The message to log.
9534
+ */
9399
9535
  debug(message) {
9400
- if (![LogLevel.DEBUG].includes(this.#logManager.logLevel)) {
9536
+ if (!this.isDebugEnabled) {
9401
9537
  return;
9402
9538
  }
9403
9539
  console.debug(`[${this.#name}] ${LogLevel.DEBUG}: ${message}`);
9404
9540
  }
9541
+ /**
9542
+ * Logs an info message.
9543
+ * @param message The message to log.
9544
+ */
9405
9545
  info(message) {
9406
- if (![LogLevel.DEBUG, LogLevel.INFO].includes(this.#logManager.logLevel)) {
9546
+ if (!this.isInfoEnabled) {
9407
9547
  return;
9408
9548
  }
9409
9549
  console.info(`[${this.#name}] ${LogLevel.INFO}: ${message}`);
9410
9550
  }
9551
+ /**
9552
+ * Logs a warn message.
9553
+ * @param message The message to log.
9554
+ */
9411
9555
  warn(message) {
9412
- if (![LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN].includes(this.#logManager.logLevel)) {
9556
+ if (!this.isWarnEnabled) {
9413
9557
  return;
9414
9558
  }
9415
9559
  console.warn(`[${this.#name}] ${LogLevel.WARN}: ${message}`);
9416
9560
  }
9561
+ /**
9562
+ * Logs an error message.
9563
+ * @param message The message to log.
9564
+ */
9417
9565
  error(message) {
9418
9566
  console.error(`[${this.#name}] ${LogLevel.ERROR}: ${message}`);
9419
9567
  }
9420
9568
  }
9421
9569
 
9422
9570
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9571
+ /**
9572
+ * Manages loggers and their log levels.
9573
+ */
9423
9574
  class VLogManager {
9575
+ /** The current log level. */
9424
9576
  #logLevel;
9577
+ /** A map of logger instances by name. */
9425
9578
  #loggers = new Map();
9426
9579
  constructor(logLevel = LogLevel.INFO) {
9427
9580
  this.#logLevel = logLevel;
9428
9581
  }
9582
+ /**
9583
+ * Sets the log level for all loggers.
9584
+ * @param level The log level to set.
9585
+ */
9429
9586
  set logLevel(level) {
9430
9587
  this.#logLevel = level;
9431
9588
  }
9589
+ /**
9590
+ * Gets the current log level.
9591
+ */
9432
9592
  get logLevel() {
9433
9593
  return this.#logLevel;
9434
9594
  }
9595
+ /**
9596
+ * Gets a logger by name, creating it if it doesn't exist.
9597
+ * @param name The name of the logger.
9598
+ * @returns The logger instance.
9599
+ */
9435
9600
  getLogger(name) {
9436
9601
  if (this.#loggers.has(name)) {
9437
9602
  return this.#loggers.get(name);
@@ -9601,9 +9766,10 @@
9601
9766
  #initializeBindings() {
9602
9767
  // Create bindings with change tracking
9603
9768
  this.#bindings = new VBindings({
9604
- onChange: (key) => {
9769
+ onChange: (identifier) => {
9605
9770
  this.#scheduleUpdate();
9606
- }
9771
+ },
9772
+ vApplication: this
9607
9773
  });
9608
9774
  // Inject utility methods into bindings
9609
9775
  this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
@@ -9665,6 +9831,15 @@
9665
9831
  }
9666
9832
  const computed = new Set();
9667
9833
  const processing = new Set();
9834
+ // Gather all changed identifiers, including parent properties for array items
9835
+ const allChanges = new Set();
9836
+ this.#bindings?.changes.forEach(id => {
9837
+ allChanges.add(id);
9838
+ const idx = id.indexOf('[');
9839
+ if (idx !== -1) {
9840
+ allChanges.add(id.substring(0, idx));
9841
+ }
9842
+ });
9668
9843
  // Helper function to recursively compute a property
9669
9844
  const compute = (key) => {
9670
9845
  // Skip if already computed in this update cycle
@@ -9680,7 +9855,7 @@
9680
9855
  // Get the dependencies for this computed property
9681
9856
  const deps = this.#computedDependencies[key] || [];
9682
9857
  // If none of the dependencies have changed, skip recomputation
9683
- if (!deps.some(dep => this.#bindings?.changes.includes(dep))) {
9858
+ if (!deps.some(dep => allChanges.has(dep))) {
9684
9859
  computed.add(key);
9685
9860
  return;
9686
9861
  }
@@ -9698,7 +9873,7 @@
9698
9873
  // Track if the computed value actually changed
9699
9874
  if (oldValue !== newValue) {
9700
9875
  this.#bindings?.set(key, newValue);
9701
- this.#logger.debug(`Computed property '${key}' changed: ${oldValue} -> ${newValue}`);
9876
+ allChanges.add(key);
9702
9877
  }
9703
9878
  }
9704
9879
  catch (error) {
@@ -9710,7 +9885,7 @@
9710
9885
  // Find all computed properties that need to be recomputed
9711
9886
  for (const [key, deps] of Object.entries(this.#computedDependencies)) {
9712
9887
  // Check if any dependency has changed
9713
- if (deps.some(dep => this.#bindings?.changes.includes(dep))) {
9888
+ if (deps.some(dep => allChanges.has(dep))) {
9714
9889
  compute(key);
9715
9890
  }
9716
9891
  }