@bian-womp/spark-graph 0.3.16 → 0.3.18

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/lib/cjs/index.cjs CHANGED
@@ -932,15 +932,166 @@ class EventEmitter {
932
932
  }
933
933
  }
934
934
 
935
+ const LOG_LEVEL_VALUES = {
936
+ debug: 0,
937
+ info: 1,
938
+ warn: 2,
939
+ error: 3,
940
+ silent: 4,
941
+ };
942
+
943
+ /**
944
+ * Shared utility functions for runtime components
945
+ */
946
+ /**
947
+ * Type guard to check if a value is a Promise
948
+ */
949
+ function isPromise(value) {
950
+ return !!value && typeof value.then === "function";
951
+ }
952
+ /**
953
+ * Unwrap a value that might be a Promise
954
+ */
955
+ async function unwrapMaybePromise(value) {
956
+ return isPromise(value) ? await value : value;
957
+ }
958
+ /**
959
+ * Shallow/deep-ish equality check to avoid unnecessary runs on identical values
960
+ */
961
+ function valuesEqual(a, b) {
962
+ if (a === b)
963
+ return true;
964
+ if (typeof a !== typeof b)
965
+ return false;
966
+ if (a && b && typeof a === "object") {
967
+ try {
968
+ return JSON.stringify(a) === JSON.stringify(b);
969
+ }
970
+ catch {
971
+ return false;
972
+ }
973
+ }
974
+ return false;
975
+ }
976
+ /**
977
+ * A reusable logger class that supports configurable log levels and prefixes.
978
+ * Can be instantiated with a default log level and optionally override per call.
979
+ */
980
+ class LevelLogger {
981
+ constructor(defaultLevel = "info", prefix = "") {
982
+ this.defaultLevel = defaultLevel;
983
+ this.prefix = prefix;
984
+ }
985
+ /**
986
+ * Sets the prefix for log messages
987
+ */
988
+ setPrefix(prefix) {
989
+ this.prefix = prefix;
990
+ }
991
+ /**
992
+ * Gets the current prefix
993
+ */
994
+ getPrefix() {
995
+ return this.prefix;
996
+ }
997
+ /**
998
+ * Sets the default log level for this logger instance
999
+ */
1000
+ setLevel(level) {
1001
+ this.defaultLevel = level;
1002
+ }
1003
+ /**
1004
+ * Gets the current default log level
1005
+ */
1006
+ getLevel() {
1007
+ return this.defaultLevel;
1008
+ }
1009
+ getLevelValue() {
1010
+ return LevelLogger.levelValues[this.defaultLevel];
1011
+ }
1012
+ /**
1013
+ * Logs a debug message
1014
+ */
1015
+ debug(message, context, overrideLevel) {
1016
+ this.log("debug", message, context, overrideLevel);
1017
+ }
1018
+ /**
1019
+ * Logs an info message
1020
+ */
1021
+ info(message, context, overrideLevel) {
1022
+ this.log("info", message, context, overrideLevel);
1023
+ }
1024
+ /**
1025
+ * Logs a warning message
1026
+ */
1027
+ warn(message, context, overrideLevel) {
1028
+ this.log("warn", message, context, overrideLevel);
1029
+ }
1030
+ /**
1031
+ * Logs an error message
1032
+ */
1033
+ error(message, context, overrideLevel) {
1034
+ this.log("error", message, context, overrideLevel);
1035
+ }
1036
+ /**
1037
+ * Core logging method that respects the log level and applies prefix
1038
+ */
1039
+ log(requestedLevel, message, context, overrideLevel) {
1040
+ const effectiveLevel = overrideLevel ?? this.defaultLevel;
1041
+ // Silent level suppresses all logs
1042
+ if (effectiveLevel === "silent") {
1043
+ return;
1044
+ }
1045
+ const requestedValue = LevelLogger.levelValues[requestedLevel] ?? 1;
1046
+ const effectiveValue = LevelLogger.levelValues[effectiveLevel] ?? 1;
1047
+ // Only log if the requested level is >= effective level
1048
+ if (requestedValue >= effectiveValue) {
1049
+ const contextStr = context
1050
+ ? ` ${Object.entries(context)
1051
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
1052
+ .join(" ")}`
1053
+ : "";
1054
+ const prefixedMessage = this.prefix
1055
+ ? `${this.prefix} ${message}${contextStr}`
1056
+ : `${message}${contextStr}`;
1057
+ switch (requestedLevel) {
1058
+ case "debug":
1059
+ console.info(prefixedMessage);
1060
+ break;
1061
+ case "info":
1062
+ console.info(prefixedMessage);
1063
+ break;
1064
+ case "warn":
1065
+ console.warn(prefixedMessage);
1066
+ break;
1067
+ case "error":
1068
+ console.error(prefixedMessage);
1069
+ break;
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ /**
1075
+ * Maps log levels to numeric values for comparison
1076
+ */
1077
+ LevelLogger.levelValues = LOG_LEVEL_VALUES;
1078
+
935
1079
  /**
936
1080
  * RunContextManager component - manages run-context lifecycle
937
1081
  */
938
1082
  class RunContextManager {
939
- constructor(graph) {
1083
+ constructor(graph, logLevel) {
940
1084
  this.graph = graph;
941
1085
  this.runContexts = new Map();
942
1086
  this.runContextCounter = 0;
943
1087
  this.graph = graph;
1088
+ this.logger = new LevelLogger(logLevel ?? "info", "[RunContextManager]");
1089
+ }
1090
+ /**
1091
+ * Set the log level for this manager
1092
+ */
1093
+ setLogLevel(logLevel) {
1094
+ this.logger.setLevel(logLevel);
944
1095
  }
945
1096
  /**
946
1097
  * Create a new run-context for runFromHere
@@ -959,6 +1110,12 @@ class RunContextManager {
959
1110
  resolve,
960
1111
  };
961
1112
  this.runContexts.set(id, ctx);
1113
+ this.logger.info("create-run-context", {
1114
+ id,
1115
+ startNodeId,
1116
+ skipPropagateValues: ctx.skipPropagateValues,
1117
+ propagate: ctx.propagate,
1118
+ });
962
1119
  return id;
963
1120
  }
964
1121
  /**
@@ -981,41 +1138,101 @@ class RunContextManager {
981
1138
  }
982
1139
  startNodeRun(id, nodeId) {
983
1140
  const ctx = this.runContexts.get(id);
984
- if (!ctx)
1141
+ if (!ctx) {
1142
+ this.logger.debug("start-node-run-context-not-found", {
1143
+ runContextId: id,
1144
+ nodeId,
1145
+ });
985
1146
  return;
1147
+ }
986
1148
  ctx.pendingNodes++;
1149
+ this.logger.debug("start-node-run", {
1150
+ runContextId: id,
1151
+ nodeId,
1152
+ pendingNodes: ctx.pendingNodes,
1153
+ });
987
1154
  }
988
1155
  finishNodeRun(id, nodeId) {
989
1156
  const ctx = this.runContexts.get(id);
990
- if (!ctx)
1157
+ if (!ctx) {
1158
+ this.logger.debug("finish-node-run-context-not-found", {
1159
+ runContextId: id,
1160
+ nodeId,
1161
+ });
991
1162
  return;
1163
+ }
992
1164
  ctx.pendingNodes--;
1165
+ this.logger.debug("finish-node-run", {
1166
+ runContextId: id,
1167
+ nodeId,
1168
+ pendingNodes: ctx.pendingNodes,
1169
+ });
993
1170
  this.finishRunContextIfPossible(id);
994
1171
  }
995
1172
  startEdgeConversion(id, edgeId) {
996
1173
  const ctx = this.runContexts.get(id);
997
- if (!ctx)
1174
+ if (!ctx) {
1175
+ this.logger.debug("start-edge-conversion-context-not-found", {
1176
+ runContextId: id,
1177
+ edgeId,
1178
+ });
998
1179
  return;
1180
+ }
999
1181
  ctx.pendingEdges++;
1182
+ this.logger.debug("start-edge-conversion", {
1183
+ runContextId: id,
1184
+ edgeId,
1185
+ pendingEdges: ctx.pendingEdges,
1186
+ });
1000
1187
  }
1001
1188
  finishEdgeConversion(id, edgeId) {
1002
1189
  const ctx = this.runContexts.get(id);
1003
- if (!ctx)
1190
+ if (!ctx) {
1191
+ this.logger.debug("finish-edge-conversion-context-not-found", {
1192
+ runContextId: id,
1193
+ edgeId,
1194
+ });
1004
1195
  return;
1196
+ }
1005
1197
  ctx.pendingEdges--;
1198
+ this.logger.debug("finish-edge-conversion", {
1199
+ runContextId: id,
1200
+ edgeId,
1201
+ pendingEdges: ctx.pendingEdges,
1202
+ });
1006
1203
  this.finishRunContextIfPossible(id);
1007
1204
  }
1008
1205
  startHandleResolution(id, nodeId) {
1009
1206
  const ctx = this.runContexts.get(id);
1010
- if (!ctx)
1207
+ if (!ctx) {
1208
+ this.logger.debug("start-handle-resolution-context-not-found", {
1209
+ runContextId: id,
1210
+ nodeId,
1211
+ });
1011
1212
  return;
1213
+ }
1012
1214
  ctx.pendingResolvers++;
1215
+ this.logger.debug("start-handle-resolution", {
1216
+ runContextId: id,
1217
+ nodeId,
1218
+ pendingResolvers: ctx.pendingResolvers,
1219
+ });
1013
1220
  }
1014
1221
  finishHandleResolution(id, nodeId) {
1015
1222
  const ctx = this.runContexts.get(id);
1016
- if (!ctx)
1223
+ if (!ctx) {
1224
+ this.logger.debug("finish-handle-resolution-context-not-found", {
1225
+ runContextId: id,
1226
+ nodeId,
1227
+ });
1017
1228
  return;
1229
+ }
1018
1230
  ctx.pendingResolvers--;
1231
+ this.logger.debug("finish-handle-resolution", {
1232
+ runContextId: id,
1233
+ nodeId,
1234
+ pendingResolvers: ctx.pendingResolvers,
1235
+ });
1019
1236
  this.finishRunContextIfPossible(id);
1020
1237
  }
1021
1238
  /**
@@ -1023,13 +1240,22 @@ class RunContextManager {
1023
1240
  */
1024
1241
  finishRunContextIfPossible(id) {
1025
1242
  const ctx = this.runContexts.get(id);
1026
- if (!ctx)
1243
+ if (!ctx) {
1244
+ this.logger.debug("finish-run-context-not-found", {
1245
+ runContextId: id,
1246
+ });
1027
1247
  return;
1248
+ }
1028
1249
  if (ctx.pendingNodes > 0 ||
1029
1250
  ctx.pendingEdges > 0 ||
1030
1251
  ctx.pendingResolvers > 0) {
1031
1252
  return; // Still has pending work
1032
1253
  }
1254
+ this.logger.info("finish-run-context", {
1255
+ runContextId: id,
1256
+ startNodes: Array.from(ctx.startNodes),
1257
+ cancelledNodes: Array.from(ctx.cancelledNodes),
1258
+ });
1033
1259
  // Clean up activeRunContexts from all nodes
1034
1260
  this.graph.forEachNode((node) => {
1035
1261
  this.graph.removeNodeRunContextId(node.nodeId, id);
@@ -1067,6 +1293,12 @@ class RunContextManager {
1067
1293
  });
1068
1294
  }
1069
1295
  }
1296
+ this.logger.debug("cancel-node-in-run-contexts", {
1297
+ nodeId,
1298
+ includeDownstream,
1299
+ cancelledNodes: Array.from(toCancel),
1300
+ affectedRunContexts: Array.from(this.runContexts.keys()),
1301
+ });
1070
1302
  // Mark nodes as cancelled in all run-contexts
1071
1303
  for (const ctx of this.runContexts.values()) {
1072
1304
  for (const id of toCancel) {
@@ -1082,6 +1314,11 @@ class RunContextManager {
1082
1314
  * Resolve all pending run-context promises (for cleanup)
1083
1315
  */
1084
1316
  resolveAll() {
1317
+ const count = this.runContexts.size;
1318
+ this.logger.info("resolve-all-run-contexts", {
1319
+ count,
1320
+ runContextIds: Array.from(this.runContexts.keys()),
1321
+ });
1085
1322
  for (const ctx of this.runContexts.values()) {
1086
1323
  if (ctx.resolve)
1087
1324
  ctx.resolve();
@@ -1091,52 +1328,12 @@ class RunContextManager {
1091
1328
  * Clear all run-contexts
1092
1329
  */
1093
1330
  clear() {
1331
+ const count = this.runContexts.size;
1332
+ this.logger.info("clear-all-run-contexts", { count });
1094
1333
  this.runContexts.clear();
1095
1334
  }
1096
1335
  }
1097
1336
 
1098
- const LOG_LEVEL_VALUES = {
1099
- debug: 0,
1100
- info: 1,
1101
- warn: 2,
1102
- error: 3,
1103
- silent: 4,
1104
- };
1105
-
1106
- /**
1107
- * Shared utility functions for runtime components
1108
- */
1109
- /**
1110
- * Type guard to check if a value is a Promise
1111
- */
1112
- function isPromise(value) {
1113
- return !!value && typeof value.then === "function";
1114
- }
1115
- /**
1116
- * Unwrap a value that might be a Promise
1117
- */
1118
- async function unwrapMaybePromise(value) {
1119
- return isPromise(value) ? await value : value;
1120
- }
1121
- /**
1122
- * Shallow/deep-ish equality check to avoid unnecessary runs on identical values
1123
- */
1124
- function valuesEqual(a, b) {
1125
- if (a === b)
1126
- return true;
1127
- if (typeof a !== typeof b)
1128
- return false;
1129
- if (a && b && typeof a === "object") {
1130
- try {
1131
- return JSON.stringify(a) === JSON.stringify(b);
1132
- }
1133
- catch {
1134
- return false;
1135
- }
1136
- }
1137
- return false;
1138
- }
1139
-
1140
1337
  function tryHandleResolving(def, registry, environment) {
1141
1338
  const out = new Map();
1142
1339
  const pending = new Set();
@@ -1335,7 +1532,7 @@ class HandleResolver {
1335
1532
  this.recomputeTokenByNode = new Map();
1336
1533
  this.environment = {};
1337
1534
  this.pendingResolutions = new Map();
1338
- this.pendingResolutionRunContexts = new Map();
1535
+ this.isRecomputing = new Map();
1339
1536
  this.environment = environment ?? {};
1340
1537
  }
1341
1538
  setRegistry(registry) {
@@ -1344,33 +1541,12 @@ class HandleResolver {
1344
1541
  setEnvironment(environment) {
1345
1542
  this.environment = environment;
1346
1543
  }
1347
- /**
1348
- * Check if handle resolution is pending for a node
1349
- */
1350
- isResolvingHandles(nodeId) {
1351
- return this.pendingResolutions.has(nodeId);
1352
- }
1353
1544
  /**
1354
1545
  * Get the promise for pending handle resolution, or null if none
1355
1546
  */
1356
1547
  getPendingResolution(nodeId) {
1357
1548
  return this.pendingResolutions.get(nodeId) || null;
1358
1549
  }
1359
- /**
1360
- * Track additional run contexts for a pending resolution
1361
- */
1362
- trackRunContextsForPendingResolution(nodeId, runContextIds) {
1363
- if (!this.pendingResolutions.has(nodeId))
1364
- return;
1365
- const tracked = this.pendingResolutionRunContexts.get(nodeId) ?? new Set();
1366
- for (const runContextId of runContextIds) {
1367
- if (!tracked.has(runContextId)) {
1368
- this.runContextManager.startHandleResolution(runContextId, nodeId);
1369
- tracked.add(runContextId);
1370
- }
1371
- }
1372
- this.pendingResolutionRunContexts.set(nodeId, tracked);
1373
- }
1374
1550
  /**
1375
1551
  * Schedule async recomputation of handles for a node
1376
1552
  */
@@ -1381,27 +1557,43 @@ class HandleResolver {
1381
1557
  const node = this.graph.getNode(nodeId);
1382
1558
  if (!node)
1383
1559
  return;
1560
+ // If already recomputing, increment token to mark that a new recompute is needed
1561
+ // but don't schedule another concurrent execution
1562
+ if (this.isRecomputing.get(nodeId)) {
1563
+ const currentToken = this.recomputeTokenByNode.get(nodeId) ?? 0;
1564
+ this.recomputeTokenByNode.set(nodeId, currentToken + 1);
1565
+ return;
1566
+ }
1384
1567
  // Track resolver start for all active run-contexts
1385
1568
  const activeRunContextIds = this.graph.getNodeRunContextIds(nodeId);
1386
- const trackedRunContextIds = new Set(activeRunContextIds);
1387
1569
  if (activeRunContextIds.size > 0) {
1388
1570
  for (const runContextId of activeRunContextIds) {
1389
1571
  this.runContextManager.startHandleResolution(runContextId, nodeId);
1390
1572
  }
1391
1573
  }
1574
+ // Mark as recomputing
1575
+ this.isRecomputing.set(nodeId, true);
1576
+ // Capture initial token before starting (will be incremented in recomputeHandlesForNode)
1577
+ const initialToken = this.recomputeTokenByNode.get(nodeId) ?? 0;
1392
1578
  // Create and track the resolution promise
1393
1579
  const resolutionPromise = new Promise((resolve) => {
1394
1580
  setTimeout(async () => {
1395
- // Get all tracked run contexts (including any added during pending state)
1396
- const allTracked = this.pendingResolutionRunContexts.get(nodeId) ?? trackedRunContextIds;
1397
- await this.recomputeHandlesForNode(nodeId, allTracked.size > 0 ? allTracked : undefined);
1581
+ await this.recomputeHandlesForNode(nodeId, activeRunContextIds);
1398
1582
  this.pendingResolutions.delete(nodeId);
1399
- this.pendingResolutionRunContexts.delete(nodeId);
1583
+ this.isRecomputing.delete(nodeId);
1584
+ // Check if a new recompute was requested while we were running
1585
+ // (token was incremented by another scheduleRecomputeHandles call)
1586
+ const finalToken = this.recomputeTokenByNode.get(nodeId) ?? 0;
1587
+ // After recomputeHandlesForNode, token should be initialToken + 1
1588
+ // If finalToken > initialToken + 1, a new recompute was requested
1589
+ if (finalToken > initialToken + 1) {
1590
+ // A new recompute was requested, schedule it now
1591
+ this.scheduleRecomputeHandles(nodeId);
1592
+ }
1400
1593
  resolve();
1401
1594
  }, 0);
1402
1595
  });
1403
1596
  this.pendingResolutions.set(nodeId, resolutionPromise);
1404
- this.pendingResolutionRunContexts.set(nodeId, trackedRunContextIds);
1405
1597
  }
1406
1598
  // Update resolved handles for a single node and refresh edge converters/types that touch it
1407
1599
  updateNodeHandles(nodeId, handles) {
@@ -2019,33 +2211,23 @@ class NodeExecutor {
2019
2211
  progress: Math.max(0, Math.min(1, Number(p) || 0)),
2020
2212
  });
2021
2213
  });
2022
- // Create log function that respects node's logLevel
2214
+ // Create log function that respects node's logLevel using LevelLogger
2215
+ const nodeLogLevel = node.logLevel ?? "info";
2216
+ const logger = new LevelLogger(nodeLogLevel, `[node:${runId || nodeId}:${node.typeId}]`);
2023
2217
  const log = (level, message, context) => {
2024
- const nodeLogLevel = node.logLevel ?? "info";
2025
- const nodeLogValue = LOG_LEVEL_VALUES[nodeLogLevel] ?? 1;
2026
- const requestedValue = LOG_LEVEL_VALUES[level] ?? 1;
2027
- // Only log if requested level >= node's logLevel
2028
- if (requestedValue >= nodeLogValue && nodeLogLevel !== "silent") {
2029
- const contextStr = context
2030
- ? ` ${Object.entries(context)
2031
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
2032
- .join(" ")}`
2033
- : "";
2034
- const fullMessage = `[node:${runId || nodeId}:${node.typeId}] ${message}${contextStr}`;
2035
- switch (level) {
2036
- case "debug":
2037
- console.info(fullMessage);
2038
- break;
2039
- case "info":
2040
- console.info(fullMessage);
2041
- break;
2042
- case "warn":
2043
- console.warn(fullMessage);
2044
- break;
2045
- case "error":
2046
- console.error(fullMessage);
2047
- break;
2048
- }
2218
+ switch (level) {
2219
+ case "debug":
2220
+ logger.debug(message, context);
2221
+ break;
2222
+ case "info":
2223
+ logger.info(message, context);
2224
+ break;
2225
+ case "warn":
2226
+ logger.warn(message, context);
2227
+ break;
2228
+ case "error":
2229
+ logger.error(message, context);
2230
+ break;
2049
2231
  }
2050
2232
  };
2051
2233
  return {
@@ -2117,23 +2299,27 @@ class NodeExecutor {
2117
2299
  }
2118
2300
  // Check if handles are being resolved - wait for resolution before executing
2119
2301
  // Do this AFTER setting up run contexts so handle resolution can track them
2120
- if (this.handleResolver && this.handleResolver.isResolvingHandles(nodeId)) {
2121
- // Track run contexts for the pending resolution
2302
+ const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
2303
+ if (pendingResolution) {
2122
2304
  if (runContextIds && runContextIds.size > 0) {
2123
- this.handleResolver.trackRunContextsForPendingResolution(nodeId, runContextIds);
2305
+ for (const id of runContextIds) {
2306
+ this.runContextManager.startHandleResolution(id, nodeId);
2307
+ }
2124
2308
  }
2125
- const pendingResolution = this.handleResolver.getPendingResolution(nodeId);
2126
- if (pendingResolution) {
2127
- // Wait for resolution to complete, then re-execute
2128
- pendingResolution.then(() => {
2129
- // Re-check node still exists and conditions
2130
- const nodeAfter = this.graph.getNode(nodeId);
2131
- if (nodeAfter) {
2132
- this.execute(nodeId, runContextIds);
2309
+ // Wait for resolution to complete, then re-execute
2310
+ pendingResolution.then(() => {
2311
+ // Re-check node still exists and conditions
2312
+ const nodeAfter = this.graph.getNode(nodeId);
2313
+ if (nodeAfter) {
2314
+ this.execute(nodeId, runContextIds);
2315
+ }
2316
+ if (runContextIds && runContextIds.size > 0) {
2317
+ for (const id of runContextIds) {
2318
+ this.runContextManager.finishHandleResolution(id, nodeId);
2133
2319
  }
2134
- });
2135
- return;
2136
- }
2320
+ }
2321
+ });
2322
+ return;
2137
2323
  }
2138
2324
  // Handle debouncing
2139
2325
  const now = Date.now();
@@ -2574,7 +2760,7 @@ class GraphRuntime {
2574
2760
  // Initialize components
2575
2761
  this.graph = new Graph();
2576
2762
  this.eventEmitter = new EventEmitter();
2577
- this.runContextManager = new RunContextManager(this.graph);
2763
+ this.runContextManager = new RunContextManager(this.graph, "debug");
2578
2764
  this.handleResolver = new HandleResolver(this.graph, this.eventEmitter, this.runContextManager, this);
2579
2765
  this.edgePropagator = new EdgePropagator(this.graph, this.eventEmitter, this.runContextManager, this.handleResolver, this);
2580
2766
  // Create NodeExecutor with EdgePropagator and HandleResolver
@@ -5709,6 +5895,7 @@ exports.CompositeCategory = CompositeCategory;
5709
5895
  exports.ComputeCategory = ComputeCategory;
5710
5896
  exports.GraphBuilder = GraphBuilder;
5711
5897
  exports.GraphRuntime = GraphRuntime;
5898
+ exports.LevelLogger = LevelLogger;
5712
5899
  exports.LocalEngine = LocalEngine;
5713
5900
  exports.Registry = Registry;
5714
5901
  exports.buildValueConverter = buildValueConverter;